YOLO:Jedis 连接池 高可用实现

本文详细介绍了如何在Java中使用Jedis连接池实现Redis的高可用性。内容涵盖简单使用Jedis、使用JedisPool连接池、通过JedisSentinelPool实现实时主从切换以及客户端分片的性能优化。示例代码展示了不同场景下的配置和操作,强调了在Redis Sentinel模式下Jedis如何处理主从切换以保持连接的高可用性。
摘要由CSDN通过智能技术生成

Jedis 连接池 高可用实现

原文
本文源代码下载:https://github.com/looyolo/looProj_test-redis.git

概述

Jedis
是 Redis 官方推荐的 Java 连接开发工具。
要在 Java 开发中使用好 Redis 中间件,
必须对 Jedis熟 悉才能写成漂亮的代码。
这篇文章不描述怎么安装 Redis 和 Redis 的命令使用,
只对 Jedis 的使用进行对介绍。

通过 Maven 获取依赖包到项目代码中:

    <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
    <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>2.9.0</version>
    </dependency>

0. Redis 环境准备

// 个人模拟内测 
//  Redis 3.2.13 版本 
//  有 2 种部署 style :
//  (1)redis-sentinel, 1主1从+3哨兵;
//  (2)redis-cluster,3主3从
//  (3)redis-single,1主

[root@toshiba redis]# 
[root@toshiba redis]# pwd
/usr/local/redis
[root@toshiba redis]# 
[root@toshiba redis]# ll
...
drwxr-xr-x 5 root root  86 3月  26 21:50 redis-cluster
drwxr-xr-x 6 root root 120 3月  26 21:47 redis-sentinel
[root@toshiba redis]# 
[root@toshiba redis]# ps -ef | grep redis
root     32516     1  0 11:36 ?        00:00:26 redis-server *:36379
root     32520     1  0 11:36 ?        00:00:24 redis-server *:36380
root     32522     1  0 11:36 ?        00:00:30 redis-sentinel *:26379 [sentinel]
root     32524     1  0 11:36 ?        00:00:30 redis-sentinel *:26380 [sentinel]
root     32532     1  0 11:36 ?        00:00:30 redis-sentinel *:26381 [sentinel]
root     13886 28342  0 15:15 pts/1    00:00:00 grep --color=auto redis
root     29824     1  0 3月26 ?       00:05:19 redis-server *:6379 [cluster]
root     29828     1  0 3月26 ?       00:04:57 redis-server *:6389 [cluster]
root     29832     1  0 3月26 ?       00:04:55 redis-server *:6380 [cluster]
root     29834     1  0 3月26 ?       00:04:58 redis-server *:6390 [cluster]
root     29838     1  0 3月26 ?       00:05:00 redis-server *:6381 [cluster]
root     29844     1  0 3月26 ?       00:04:58 redis-server *:6391 [cluster]
root     22577     1  0 17:38 ?        00:00:00 redis-server *:11111
[root@toshiba redis]# 
// 通过 RedisDesktopManger 工具,向 redis 中写入缓存数据
key:
qwbqk:10014:385:-1:17968
valuse:
{
  "TAG_ID": "17968",
  "KEY_VALUE": "385",
  "ORG_ID": "-1",
  "TAG_COMMENT": "make a comment",
  "CONFIDENCE_LEVEL": "94",
  "TAGOBJ_ID": "10014",
  "UPDATE_TIME": "2021-01-29 14:22:22",
  "EFFECT_TIME": "2021-01-29 14:31:11"
}

// 或者,通过 命令行操作(需腰 转义 处理),向 redis 中写入缓存数据
set qwbqk:10014:385:-1:17968 "{\n  \"TAG_ID\": \"17968\",\n  \"KEY_VALUE\": \"385\",\n  \"ORG_ID\": \"-1\",\n  \"TAG_COMMENT\": \"make a comment\",\n  \"CONFIDENCE_LEVEL\": \"94\",\n  \"TAGOBJ_ID\": \"10014\",\n  \"UPDATE_TIME\": \"2021-01-29 14:22:22\",\n  \"EFFECT_TIME\": \"2021-01-29 14:31:11\"\n}"

1. 简单使用

Jedis的 基本使用非常简单,
只需要创建 Jedis 对象的时候指定 host,port, password 即可。
当然,Jedis 对象又很多构造方法,都大同小异,
只是对应和 Redis 连接的 socket 的参数不一样而已。

import redis.clients.jedis.Jedis;

public class ClientJedis {
    public static void main(String[] args) {

        /**
         * 简单使用
         */
        // 创建 jedis 对象,指定 redis 服务 host、port
        Jedis jedis = null;
        try {
            // 1:从 连接池 获取 连接对象
            //  如果访问 redis 服务需要密码,则指定密码
            jedis = new Jedis("toshiba",6379);
            jedis.auth("passwd");
            // 2:访问 redis 服务
            String key = "qwbqk:10014:385:-1:17968";
            String value = jedis.get(key);
            System.out.println("key: " + key + " ,\n" + "value:" + value);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 3:使用完关闭 redis 服务
            if (jedis != null) jedis.close();
        }
    }
}

符合预期的运行结果,如下,
在这里插入图片描述
Jedis 基本使用十分简单,
在每次使用时,构建 Jedis 对象即可。
在 Jedis 对象构建好之后,Jedis 底层会打开一条 Socket 通道
和 Redis 服务进行连接。所以在使用完 Jedis 对象之后,
需要调用 Jedis.close() 方法把连接关闭,不然会占用系统资源。
当然,如果应用非常频繁的创建和销毁 Jedis 对象,
性能占用是很大影响的,因为构建 Socket 通道 是很耗时的
(类似于构建数据库 jdbc 连接) 。
我们应该使用 连接池 来减少 Socket 对象的创建和销毁过程。

2. 使用 连接池 + Cluster 实现 HA

JedisPool 是基于 org.apache.commons.pool2 实现的。
在构建连接池对象的时候,需要提供池对象的配置对象
JedisPoolConfig (继承自 GenericObjectPoolConfig )。
我们可以通过这个配置对象对连接池进行相关参数的配置
(如最大连接数,最大空闲数、连接超时等)。

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

public class ClientJedis {
    public static void main(String[] args) {
        /**
         * 使用 JedisPool连接池,搭配 redis-cluster 实现 HA
         *
         * 适用于 redis-cluster,3主3从
         */
        // 1:创建 连接池配置对象
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(30);    // 指定最大空闲连接数
        jedisPoolConfig.setMaxTotal(100);    // 指定最大连接数
        jedisPoolConfig.setMaxWaitMillis(60000);    // 指定最大等待时间
        // 2:创建 连接池对象,关联 连接池配置对象
        JedisPool jedisPool = new JedisPool(jedisPoolConfig,
                "toshiba",6379, 60000,"passwd");
        Jedis jedis = null;
        try {
            // 3:从 连接池 获取 连接对象
            jedis = jedisPool.getResource();
            // 4:访问 redis 服务
            String key = "qwbqk:10014:385:-1:17968";
            String value = jedis.get(key);
            System.out.println("key: " + key + " ,\n" + "value:" + value);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 5:使用完关闭 redis 服务
            if (jedis != null) jedis.close();
        }
    }
}

符合预期的运行结果,如下,
在这里插入图片描述

使用 Jedis 连接池之后,从对象池中获取连接时,
将会对 dataSource 进行设置,
在每次用完连接对象后,
一定要记得把连接归还给连接池。
Jedis 对 close 方法进行了改造,如果是连接池中的连接对象,
调用 close 方法将会是把连接对象返回到对象池,
若不是则关闭连接。

可以查看源码,如下,

-----------------------------------------------------------------
	// 摘自源码 JedisPool.class
    public Jedis getResource() {
        Jedis jedis = (Jedis)super.getResource();    
        jedis.setDataSource(this);
        return jedis;
    }
-----------------------------------------------------------------
	// 摘自源码 jedis.class 
    public void close() {    // Jedis 的 close 方法
        if (this.dataSource != null) {
            if (this.client.isBroken()) {
                this.dataSource.returnBrokenResource(this);
            } else {
                this.dataSource.returnResource(this);
            }
        } else {
            this.client.close();
        }
    }

3. 使用 JedisSentinelPool 实现 HA

我们知道,连接池可以大大提高应用访问 Reids 服务的性能,
减去大量的 Socket 的 创建 和 销毁 过程。
但是 Redis 为了保障高可用,
服务一般都是 (Redis-Sentinel 模式)。
当 Redis 服务中的主服务挂掉之后,
会仲裁(类似于 选举)出另外一台 Slave 服务充当 Master 。
这个时候,我们的应用即使使用了 Jedis 连接池,
Master 服务挂了,应用程序还是无法连接新的 Master 服务。
为了解决这个问题,Jedis 也提供了相应的 SentinelPool 实现
能够在 Redis Sentinel 主从时,通知我们的应用,
把我们的应用连接到新的 Master 服务。
前提是 Redis 集群已有 Sentinel 。

注意:Jedis 版本必须 2.4.2 或更新版本

sentinel 高可用

·    sentinel 的高可用是服务端的高可用;
·    服务端的 master 挂了可以完成故障转移,客户端如果感知不到这个转移是没有作用的;

客户端高可用基本原理

1:    client 拿着 sentinel 节点集合 + materName,遍历 sentinel 集合,获取一个可用的 sentinel 节点;
2:    client 拿着 masterName 向获取到的可用的 sentinel 节点要 master 的地址;:3:    client 拿着 master 的地址验证一下其到底是不是 master;
4:    如果 master 发生的转移,sentinel 是可以感知的,client 和 sentinel 之间的通知是通过发布订阅模式,client 订阅了 sentinel 的某个频道,频道中有 master 的变化,如果 master 发生了变化,就会在这个频道中发布一条消息,订阅的 client 就可以获取,在取新的 master 进行连接;

客户端接入流程

1:    sentinel ip:port 集合;
2:    masterName;
3:    不是代理模式,只有第一次连接的时候通过 sentinel,后面直接连 master 了;

先看下怎么使用,JedisSentinelPool 示例,如下,

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisSentinelPool;

import java.util.HashSet;
import java.util.Set;

public class ClientJedis {
    public static void main(String[] args) {
        /**
         * 使用 JedisSentinelPool 实现 HA
         *
         * 适用于 redis-sentinel,1主1从+3哨兵
         * 其中,主节点名称 默认 mymaster
         */
        // 1:创建 连接池配置对象
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(30);
        jedisPoolConfig.setMaxIdle(300);
        jedisPoolConfig.setMaxWaitMillis(60000);
        // 2:创建 哨兵集合对象
        Set<String> jedisSentinels = new HashSet<String>();
        jedisSentinels.add("toshiba:26379");
        jedisSentinels.add("toshiba:26380");
        jedisSentinels.add("toshiba:26381");
        // 3:创建 哨兵连接池对象,关联 主节点名称、哨兵集合对象、连接池配置对象
        JedisSentinelPool jedisSentinelPool = new JedisSentinelPool("mymaster",jedisSentinels,jedisPoolConfig);
        Jedis jedis = null;
        try {
            // 4:从 连接池 获取 连接对象
            jedis = jedisSentinelPool.getResource();
            // 5:访问 redis 服务
            String key = "qwbqk:10014:385:-1:17968";
            String value = jedis.get(key);
            System.out.println("key: " + key + " ,\n" + "value:" + value);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 6:使用完关闭 redis 服务
            if (jedis != null) jedis.close();
        }

    }
}

符合预期的运行结果,如下,
在这里插入图片描述
Jedis Sentinel 的使用也是十分简单的,
只是在 JedisPool 中添加了 Sentinel 和 MasterName 参数。
Jedis Sentinel 基于 Redis 订阅实现 Redis 主从服务切换通知
当 Reids 发生主从切换时,
Sentinel 会发送通知主动通知 Jedis 进行连接的切换。
JedisSentinelPool 在每次从连接池中获取链接对象的时候,
都要对连接对象进行检测,
如果此链接和 Sentinel 的 Master 服务连接参数不一致,
则会关闭此连接,重新获取新的 Jedis连 接对象。

-----------------------------------------------------------------
    // 摘自源码 jedisSentinelPool.class
    public Jedis getResource() {
        while(true) {
            Jedis jedis = (Jedis)super.getResource();
            jedis.setDataSource(this);
            HostAndPort master = this.currentHostMaster;
            HostAndPort connection = new HostAndPort(jedis.getClient().getHost(), jedis.getClient().getPort());
            if (master.equals(connection)) {
                return jedis;
            }

            this.returnBrokenResource(jedis);
        }
    }

JedisSentinelPool 对象要实时监控 RedisSentinel 的主从切换。
在其内部通过 Reids 的订阅实现。
具体的实现看 JedisSentinelPool 的两个方法就很清晰。

-----------------------------------------------------------------
    private HostAndPort initSentinels(Set<String> sentinels, String masterName) {
        HostAndPort master = null;
        boolean sentinelAvailable = false;
        this.log.info("Trying to find master from available Sentinels...");
        Iterator var5 = sentinels.iterator();

        String sentinel;
        HostAndPort hap;
        while(var5.hasNext()) {
            sentinel = (String)var5.next();
            hap = HostAndPort.parseString(sentinel);
            this.log.fine("Connecting to Sentinel " + hap);
            Jedis jedis = null;

            try {
                jedis = new Jedis(hap.getHost(), hap.getPort());
                // 从 RedisSentinel 中获取 Master 信息
                List<String> masterAddr = jedis.sentinelGetMasterAddrByName(masterName);
                sentinelAvailable = true;
                if (masterAddr != null && masterAddr.size() == 2) {
                    master = this.toHostAndPort(masterAddr);
                    this.log.fine("Found Redis master at " + master);
                    break;
                }

                this.log.warning("Can not get master addr, master name: " + masterName + ". Sentinel: " + hap + ".");
            } catch (JedisException var13) {
                this.log.warning("Cannot get master address from sentinel running @ " + hap + ". Reason: " + var13 + ". Trying next one.");
            } finally {
                if (jedis != null) {
                    jedis.close();
                }

            }
        }

        if (master == null) {
            if (sentinelAvailable) {
                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...");
            }
        } else {
            this.log.info("Redis master running at " + master + ", starting Sentinel listeners...");
            var5 = sentinels.iterator();

            // 启动 后台线程 监控 RedisSentinal 的主从切换通知
            while(var5.hasNext()) {
                sentinel = (String)var5.next();
                hap = HostAndPort.parseString(sentinel);
                JedisSentinelPool.MasterListener masterListener = new JedisSentinelPool.MasterListener(masterName, hap.getHost(), hap.getPort());
                // whether MasterListener threads are alive or not, process can be stopped
                masterListener.setDaemon(true);
                this.masterListeners.add(masterListener);
                masterListener.start();
            }

            return master;
        }
    }

-----------------------------------------------------------------
    private void initPool(HostAndPort master) {
        if (!master.equals(this.currentHostMaster)) {
            this.currentHostMaster = master;
            if (this.factory == null) {
                this.factory = new JedisFactory(master.getHost(), master.getPort(), this.connectionTimeout, this.soTimeout, this.password, this.database, this.clientName, false, (SSLSocketFactory)null, (SSLParameters)null, (HostnameVerifier)null);
                this.initPool(this.poolConfig, this.factory);
            } else {
                this.factory.setHostAndPort(this.currentHostMaster);
                // although we clear the pool, we still have to check the returned object
                // in getResource, this call only clears idle instances, not
                // borrowed instances
                this.internalPool.clear();
            }

            this.log.info("Created JedisPool to master at " + master);
        }

    }

可以看到,Jedis Sentinel 监控是用 MasterListener 对象来实现。
看对应源码可以发现是基于 Redis 的 订阅 实现的,
其订阅频道为 "+switch-master "。
当 MasterListener 接收到 switch-master 消息时候,
会使用新的 Host和port 进行 initPool 。
这样对连接池中的连接对象清除,
重新创建新的连接指向新的 Maste 服务。
在这里插入图片描述

4. 对比 Redis 单机版 性能优化:客户端分片

对于大应用来说,单台 Redis 服务器肯定满足不了应用的需求。
在 Redis3.0 之前,是不支持集群的。
如果要使用多台 Reids 服务器,必须采用其他方式。
很多公司使用了代理方式来解决 Redis 集群。
对于 Jedis ,也提供了 客户端分片 的模式来连接 “Redis集群”。
其内部是采用 Key 的一致性 hash 算法,
来区分 key 存储在哪个 Redis 实例上的。

JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(500);
config.setTestOnBorrow(true);
List<JedisShardInfo> jdsInfoList = new ArrayList<>(2);
jdsInfoList.add(new JedisShardInfo("192.168.2.128", 6379));
jdsInfoList.add(new JedisShardInfo("192.168.2.108", 6379));
pool = new ShardedJedisPool(config, jdsInfoList, Hashing.MURMUR_HASH, Sharded.DEFAULT_KEY_TAG_PATTERN);
jds.set(key, value);
......
jds.close();
pool.close();

符合预期的运行结果,如下,

<1>:分片中有一台 Redis 服务主机发生故障时,
在这里插入图片描述
<2>:分片中所有 Redis 服务主机均都正常时,
在这里插入图片描述
当然,采用这种方式也存在 2 个明显问题:

1:扩容问题:
因为使用了一致性哈稀进行分片,
那么不同的 key 分布到不同的 Redis-Server 上,
当我们需要扩容时,需要增加机器到分片列表中,
这时候会使得同样的 key 算出来落到跟原来不同的机器上,
这样如果要取某一个值,会出现取不到的情况。

2:单点故障问题:
当集群中的某一台服务挂掉之后,
客户端在根据一致性 hash 无法从这台服务器取数据。

对于扩容问题,
Redis 的作者提出了一种名为 Pre-Sharding 的方式,
即事先部署足够多的 Redis 服务。

对于单点故障问题,
我们可以使用 Redis 的 HA 高可用来实现。
利用 Redis-Sentinal 来通知主从服务的切换。
当然,Jedis 没有实现这块。

5. 小结

对于 Jedis 的基本使用还是很简单的。
要根据不用的应用场景选择对于的使用方式。

另外,Spring 也提供了 Spring-data-redis 包来整合 Jedis 的操作,
Spring 也单独封装了 Jedis 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

looyolo

你的鼓励,是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值