1. 高可用连接JedisSentinelPoll
Redis Sentinel是Redis官方提供的高可用解决方案,提供监控、通知和自动故障转移三个主要功能。
Redis Sentinel一般主要用于管理Redis服务器,主要是监控redis多台服务器,并在master宕机后,自动执行故障转移,从slave中选举出一台服务升级为新的master。
Redis Sentinel的详细介绍可以参考Redis 哨兵模式Sentinel
Redis Sentinel的部署可以参考Redis 哨兵模式部署章节
1.1. Jedis Sentinel 代码示例
Jedis客户端也提供了Redis Sentinel的支持,使用Jedis的Sentinel模式很简单,先看一下示例代码:
public class JedisSentinelsStartApp {
public static final Logger LOGGER = LoggerFactory.getLogger(JedisSentinelsStartApp.class);
public static void main(String[] args) {
// redis 哨兵服务列表
Set<String> sentinels = new HashSet<String>();
sentinels.add("127.0.0.1:26379");
sentinels.add("127.0.0.1:26380");
sentinels.add("127.0.0.1:26381");
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(8);
jedisPoolConfig.setMaxTotal(20);
// 创建一个jedis哨兵连接池
JedisSentinelPool sentinelPool = new JedisSentinelPool("mymaster", sentinels, jedisPoolConfig);
Jedis jedis = sentinelPool.getResource();
jedis.set("sentinel-key1", "value1");
jedis.set("sentinel-key2", "value2");
Jedis jedis1 = new Jedis("localhost", 6379);
String valueOfKey1 = jedis1.get("sentinel-key1");
LOGGER.info("valueOfKey1:{}", valueOfKey1);
String valueOfKey2 = jedis1.get("sentinel-key2");
LOGGER.info("valueOfKey2:{}", valueOfKey2);
jedis1.close();
jedis.close();
sentinelPool.close();
}
}
使用Jedis Sentinel模式的主要步骤:
- 创建一个HashSet,存储redis sentinel服务器列表
- 创建
JedisPoolConfig
配置对象 - 创建
JedisSentinelPool
连接池对象,构造函数需要提供一个sentinel指定的mastername - 从连接池中获取一个Jedis对象
- 使用Jedis对象操作Redis
- 把jedis连接对象归还到连接池
- 关闭连接池
看一下上面代码的日志:
十月 13, 2017 10:49:39 上午 redis.clients.jedis.JedisSentinelPool initSentinels
信息: Trying to find master from available Sentinels...
十月 13, 2017 10:49:39 上午 redis.clients.jedis.JedisSentinelPool initSentinels
信息: Redis master running at 127.0.0.1:6380, starting Sentinel listeners...
十月 13, 2017 10:49:39 上午 redis.clients.jedis.JedisSentinelPool initPool
信息: Created JedisPool to master at 127.0.0.1:6380
10:49:39.335 [main] INFO samples.cache.jedis.JedisSentinelsStartApp - valueOfKey1:value1
10:49:39.339 [main] INFO samples.cache.jedis.JedisSentinelsStartApp - valueOfKey2:value2
可以发现,JedisSentinelPool会获取当前的master地址,上述示例为127.0.0.1:6380
1.2. 原码分析
JedisSentinelPool的底层是基于Redis提供的下面功能实现:
- SENTINEL get-master-addr-by-name 命令来获取 master 地址;
- Pub/Sub 发布订阅功能,订阅sentinels发布的
+switch-master
频道,当redis主从切换时,redis会在此频道发送一条消息,jedis订阅此频道,获取到此消息后,进行本地master更新。
public JedisSentinelPool(String masterName, Set<String> sentinels,
final GenericObjectPoolConfig poolConfig, final int connectionTimeout, final int soTimeout,
final String password, final int database, final String clientName) {
this.poolConfig = poolConfig;
this.connectionTimeout = connectionTimeout;
this.soTimeout = soTimeout;
this.password = password;
this.database = database;
this.clientName = clientName;
HostAndPort master = initSentinels(sentinels, masterName);
initPool(master);
}
private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
HostAndPort master = null;
boolean sentinelAvailable = false;
log.info("Trying to find master from available Sentinels...");
for (String sentinel : sentinels) {
final HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":")));
log.fine("Connecting to Sentinel " + hap);
Jedis jedis = null;
try {
jedis = new Jedis(hap.getHost(), hap.getPort());
List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
// connected to sentinel...
sentinelAvailable = true;
if (masterAddr == null || masterAddr.size() != 2) {
log.warning("Can not get master addr, master name: " + masterName + ". Sentinel: " + hap
+ ".");
continue;
}
master = toHostAndPort(masterAddr);
log.fine("Found Redis master at " + master);
break;
} catch (JedisException e) {
// resolves #1036, it should handle JedisException there's another chance
// of raising JedisDataException
log.warning("Cannot get master address from sentinel running @ " + hap + ". Reason: " + e
+ ". Trying next one.");
} finally {
if (jedis != null) {
jedis.close();
}
}
}
if (master == null) {
if (sentinelAvailable) {
// can connect to sentinel, but master name seems to not
// monitored
throw new JedisException("Can connect to sentinel, but " + masterName
+ " seems to be not monitored...");
} else {
throw new JedisConnectionException("All sentinels down, cannot determine where is "
+ masterName + " master is running...");
}
}
log.info("Redis master running at " + master + ", starting Sentinel listeners...");
for (String sentinel : sentinels) {
final HostAndPort hap = toHostAndPort(Arrays.asList(sentinel.split(":")));
MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
// whether MasterListener threads are alive or not, process can be stopped
masterListener.setDaemon(true);
masterListeners.add(masterListener);
masterListener.start();
}
return master;
}
可以看到,JedisSentinelPool的构造函数中,主要做了几件事:
- 循环所有的sentinels,使用Jedis连接sentinel服务器,调用
jedis.sentinelGetMasterAddrByName(masterName)
来获取master服务器地址
底层使用的是public List<String> sentinelGetMasterAddrByName(String masterName) { client.sentinel(Protocol.SENTINEL_GET_MASTER_ADDR_BY_NAME, masterName); final List<Object> reply = client.getObjectMultiBulkReply(); return BuilderFactory.STRING_LIST.build(reply); }
SENTINEL
的get-master-addr-by-name
命令:$ bin/redis-cli -p 26379 sentinel get-master-addr-by-name mymaster 1) "127.0.0.1" 2) "6380"
- 如果sentinel正确返回master地址,则执行下一步,否则使用下一个sentinel重新第1,第2步
- 如果循环完所有的sentinels都没有返回正确的master地址,根据sentinel是否可用,抛出异常,否则执行步骤4
如果master已获取到,则添加一个监听器MasterListener,此监听器主要用来订阅redis发布的master变更频道:
+switch-master
,核心代码:j.subscribe(new JedisPubSub() { @Override public void onMessage(String channel, String message) { log.fine("Sentinel " + host + ":" + port + " published: " + message + "."); String[] switchMasterMsg = message.split(" "); if (switchMasterMsg.length > 3) { if (masterName.equals(switchMasterMsg[0])) { initPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4]))); } else { log.fine("Ignoring message on +switch-master for master name " + switchMasterMsg[0] + ", our master name is " + masterName); } } else { log.severe("Invalid message received on Sentinel " + host + ":" + port + " on channel +switch-master: " + message); } } }, "+switch-master");
redis 发布的 +switch-master 频道消息格式:
+switch-master mymaster 127.0.0.1 6379 127.0.0.1 6380
所以上述代码中的
initPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
是以新master地址初始化连接池。看一下JedisSentinel的getResource实现
public Jedis getResource() { while (true) { Jedis jedis = super.getResource(); jedis.setDataSource(this); // get a reference because it can change concurrently final HostAndPort master = currentHostMaster; final HostAndPort connection = new HostAndPort(jedis.getClient().getHost(), jedis.getClient() .getPort()); if (master.equals(connection)) { // connected to the correct master return jedis; } else { returnBrokenResource(jedis); } } }
JedisSentinelPool每次从连接池获取链接对象的时候,都要对连接对象进行检测,如果此对象和sentinel的master服务地址不一致,则会关闭此连接,重新获取新的Jedis连接对象。