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 。