因为最近工作上,需要更新项目的缓存需求, 将原有的集群直连Redis更换为接入CacheClound的云管理平台,实现Redis高可用;
参考文章链接:https://www.jianshu.com/p/8fa88c0e85ba https://blog.youkuaiyun.com/hyl29742/article/details/86561060
CacheClound作为搜狐TV开源的一个管理Redis的云平台,对于运维来说管理缓存配置,故障预警,故障转移等一些列优势.
目前工作项目现在接入CacheClound需要采用Sentinel 的Redis部署策略接入,首先先看jedis的Java客户端中,对Sentinel 的支持;
Sentinel 构造方法: 对一些配置项的初始化,
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);
}
构造器中 初始化 Sentinel 和 Pool ,另外 还有一个initSentinel 方法;
private HostAndPort initSentinels(Set<String> sentinels, final String masterName) {
HostAndPort master = null;
boolean sentinelAvailable = false;
for (String sentinel : sentinels) {// 遍历 Sentinel 节点
final HostAndPort hap = HostAndPort.parseString(sentinel);// 解析 String 为 ip port
Jedis jedis = null;
try {
jedis = new Jedis(hap.getHost(), hap.getPort());// 创建 Jedis 对象
// 执行 get-master-addr-by-name masterName 获取主节点信息
List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
// 标识符:Sentinel 存在
sentinelAvailable = true;
// 如果主节点是空,或者返回长度不等于 2,跳过此 Sentinel
if (masterAddr == null || masterAddr.size() != 2) {
continue;
}
// 如果成功获取主节点,解析字符串成 HostAndPort 对象
master = toHostAndPort(masterAddr);
break;
} catch (JedisException e) {
} finally {
if (jedis != null) {
jedis.close();// 每次循环归还 Redis 连接
}
}
}
// 如果 master 是 null,抛出异常
if (master == null) {
if (sentinelAvailable) {// 细化异常,这个是 Sentinel 有问题
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...");
}
}
// 遍历 哨兵
for (String sentinel : sentinels) {
final HostAndPort hap = HostAndPort.parseString(sentinel);
// 创建 master 监听器线程
MasterListener masterListener = new MasterListener(masterName, hap.getHost(), hap.getPort());
masterListener.setDaemon(true);// 后台线程
masterListeners.add(masterListener);// 添加到监听集合,后期优雅关闭
masterListener.start();// 启动线程
}
return master;
}
这个 方法总结下来是这样的逻辑:
①遍历 Sentinel 字符串
②根据字符串生成 HostAndPort 对象,然后创建一个 Jedis 对象。
③使用 Jedis 对象发送 get-master-addr-by-name masterName
命令,得到 master 信息。
④得到 master 信息后,再次遍历哨兵集合,为每个哨兵创建一个线程,监听哨兵的发布订阅消息,消息主题是 +switch-master
. 当主节点发生变化时,将通过 pub/sub 通知该线程,该线程将更新 Redis 连接池。
同时开启的线程中我们可以看下在线程中做了什么操作:
@Override
public void run() {
// flag
running.set(true);
// 死循环
while (running.get()) {
//创建一个 Jedis对象
j = new Jedis(host, port);
try {
// 继续检查
if (!running.get()) {
break;
}
// jedis 对象,通过 Redis pub/sub 订阅 switch-master 主题
j.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
// 分割字符串
String[] switchMasterMsg = message.split(" ");
// 如果长度大于三
if (switchMasterMsg.length > 3) {
// 且 第一个 字符串的名称和当前 masterName 发生了 switch
if (masterName.equals(switchMasterMsg[0])) {
// 重新初始化连接池(第 4 个和 第 5 个)
initPool(toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
} else {
}
} else {
}
}
}, "+switch-master");
} catch (JedisConnectionException e) {
// 如果连接异常
if (running.get()) {
try {
// 默认休息 5 秒
Thread.sleep(subscribeRetryWaitTimeMillis);
} catch (InterruptedException e1) {
}
} else {
}
} finally {
j.close();
}
}
}
总的来说就是 :
根据哨兵的 host 和 port 创建一个 jedis 对象,然后,这个 jedis 对象订阅了 pub/sub 消息,,消息的主题是 "+switch-master" ,如果收到消息了,就执行 onMessage 方法,该方法会根据新的 master 信息重新初始化 Redis 连接池。
对于初始化操作:
private void initPool(HostAndPort master) {
// 比较 host + port,如果不相等,就重新初始化
if (!master.equals(currentHostMaster)) {
// 修改当前 master
currentHostMaster = master;
if (factory == null) {
factory = new JedisFactory(master.getHost(), master.getPort(), connectionTimeout,
soTimeout, password, database, clientName, false, null, null, null);
initPool(poolConfig, factory);
} else {
// 修改连接参数, 下次获取连接的时候,就可以生成新的连接
factory.setHostAndPort(currentHostMaster);
// 清空旧的连接池
internalPool.clear();
}
}
}
在 Sentinel 构造器里面,也会调用这个方法,第一次调用的时候, factory 肯定是 null,第二次调用的时候,会设置 factory 的 hostAndPort 为新的 master 地址,然后清空原来的连接池。那么新的 getResource 方法就会从这个新的地址获取到新的连接了。
总结:
1. 该类在初始化的时候,会将监视redis主从的sentinels放入一个list。
2.遍历该list,向sentinel发送一个命令:获取指定masterName的地址,而且只要有一个sentinel响应就终止循环。
3.根据上面获取的masters地址,与masters建立连接并初始化GenericObjectPool,由它来获取Jedis实例。
4.与此同时,客户端与sentinel之间,创建一个消息订阅,如果sentinel所监控的master有改变,则立刻触发initPool方法,重置master的地址。
5.由此可以解释,为什么master的客户端连接数比sentinel高,sentinel只是监视作用,告知你master在哪里,实际上还是得直接与master创建连接。