说明
在学习研究Redis集群部署的过程中,发现以哨兵模式部署集群时,使用Jedis作为客户端只可以连接到主机,从机只作为备份保证高可用。这样读写都在主机,在读比较高的情况下对主机带来很大压力。通过阅读Jedis的JedisSentinelPool源码,在该类的基础上实现JedisSentinelMasterSlavePool类,通过该类实现redis 哨兵模式下的读操作负载均衡。
正文
基础知识
关于redis集群的基础知识,这里先不做总结,可以看以下资料进行学习和搭建:
深入剖析Redis系列(一) - Redis入门简介与主从搭建
深入剖析Redis系列(二) - Redis哨兵模式与高可用集群
深入剖析Redis系列(三) - Redis集群模式搭建与原理详解
深入剖析Redis系列(四) - Redis数据结构与全局命令概述
深入剖析Redis系列(五) - Redis数据结构之字符串
深入剖析Redis系列(六) - Redis数据结构之哈希
深入剖析Redis系列(七) - Redis数据结构之列表
深入剖析Redis系列(八) - Redis数据结构之集合
在哨兵模式下,一般部署三个节点作为哨兵集群,保证哨兵的高可用。同时每个master节点都是采用主从复制的模式,写操作在master,再将数据同步到slave节点。这里的哨兵用来监测master节点的状态,保证在master节点无法正常工作时,能够自动故障转移从slave节点中选取新的master节点,当旧master节点恢复正常后,可以作为新的slave节点重新加入集群。
JedisSentinelPool
该类是Jedis支持redis 哨兵模式的连接池,在该类中持有个GenericObjectPool对象,初始化该类时会创建一个主机连接池,同时会创建一个MasterListener,当发生故障转移时会重新初始化主机连接池。在MasterListener监听器中主要订阅监听了+switch-master通道,当发生主机切换时,哨兵会通过此通道发送同名事件,通过监听该事件JedisSentinelPool实现连接池的重新初始化。
更多事件详见官方文档
this.j.subscribe(new JedisPubSub() {
public void onMessage(String channel, String message) {
JedisSentinelPool.this.log.debug("Sentinel {}:{} published: {}.", new Object[]{MasterListener.this.host, MasterListener.this.port, message});
String[] switchMasterMsg = message.split(" ");
if (switchMasterMsg.length > 3) {
if (MasterListener.this.masterName.equals(switchMasterMsg[0])) {
JedisSentinelPool.this.initPool(JedisSentinelPool.this.toHostAndPort(Arrays.asList(switchMasterMsg[3], switchMasterMsg[4])));
} else {
JedisSentinelPool.this.log.debug("Ignoring message on +switch-master for master name {}, our master name is {}", switchMasterMsg[0], MasterListener.this.masterName);
}
} else {
JedisSentinelPool.this.log.error("Invalid message received on Sentinel {}:{} on channel +switch-master: {}", new Object[]{MasterListener.this.host, MasterListener.this.port, message});
}
}
}, new String[]{"+switch-master"});
基于以上特性,通过改写JedisSentinelPool使其在拥有主机连接池的情况下,携带从机连接池,并改写监听器监听其他事件,使能够主从机变化时主从连接池跟随变化。
JedisSentinelMasterSlavePool
在JedisSentinelPool类的基础上实现该类。添加了从机连接池地址列表slavesAddr,从机连接池集合Map<HostAndPort, GenericObjectPool<jedis>> slavePools,改写MasterListener,增加监听+slave, +sdown, -sdown事件,使得在发生主机切换,从机上下线时能自动改变连接池。同时添加一个ThreadLocal<GenericObjectPool<Jedis>> objectPoolThreadLocal变量,主要是为了能够归还资源,在通过主机获取jedis对象时设置了DataSource,关闭时jedis通过该变量获取连接池对象,连接池pool使用returnResource()方法归还资源。根据此特性,添加ThreadLocal变量,当从slave获取资源时保存该线程从哪个从机连接池获取的Jedis,归还资源时可以找到对应的连接池。
以这种方式实现读操作在从机上的负载均衡,当集群状态发生变化,连接池也跟随变化,可能造成无法获取连接的情况,需要做容错处理。
这里从机连接的获取使用了随机算法,也可以使用其他算法。
新增变量
private volatile Map<HostAndPort, GenericObjectPool<Jedis>> slavePools;
private volatile List<HostAndPort> slavesAddr;
private final Object changeSlavePoolLock;
private final ThreadLocal<GenericObjectPool<Jedis>> objectPoolThreadLocal = new ThreadLocal<>();
private volatile JedisFactory2 factory;
由于JedisFactory属于包内资源,要新建一个JedisFactory2。
使用随机算法获取从机连接
public Jedis getSlaveResource() {
try{
if (this.slavePools != null && this.slavePools.size() > 0) {
Random random = new Random();
HostAndPort slaveHP = this.slavesAddr.get(random.nextInt(slavePools.size()));
GenericObjectPool<Jedis> pool = this.slavePools.get(slaveHP);
this.log.info("Get a slave pool, the address is {}", slaveHP);
objectPoolThreadLocal.set(pool);
return pool.borrowObject();
}
}catch(Exception e){
this.log.debug("Could not get a resource form slave pools");
}
return this.getResource();
}
归还从机连接池资源
public void closeSlaveJedis(Jedis jedis) {
GenericObjectPool<Jedis> pool = objectPoolThreadLocal.get();
//是否为从机的连接
if (pool != null) {
pool.returnObject(jedis);
} else {
jedis.close();
}
}
修改监听器,添加其他监听事件
//重写监听机制 当发现主机切换时,重新初始化主机的连接池。当发现新从机上线(作为旧主机故障恢复,重新上线成为从机),添加新从机到从机连接池
//还有从机的主观下线时 需要将其删除
this.j.subscribe(new JedisPubSub() {
public void onMessage(String channel, String message) {
JedisSentinelMasterSlavePool.this.log.debug("Sentinel {}:{}, channel is {} == published: {}.", new Object[]{JedisSentinelMasterSlavePool.MasterListener.this.host, JedisSentinelMasterSlavePool.MasterListener.this.port, channel, message});
String[] switchMasterMsg = message.split(" ");
if (switchMasterMsg.length > 3 &a