Redis学习(一):redis集群之哨兵模式下的负载均衡

说明

在学习研究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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值