客户端原理
大多数开发语言的Redis客户端都采用Smart客户端支持集群协议,从中找出符合自己要求的客户端类库。Smart客户端通过在内部维护slot->node的映射关系,本地就可实现键到节点的查找,从而保证IO效率的最大化,而MOVED重定向负责协助Smart客户端更新slot->node映射。Smart客户端操作集群的流程如下:
1)首先在JedisCluster初始化是会选择一个运行节点,初始化槽和节点映射关系,使用cluster slots命令完成。
2)Jedis Cluster解析cluster slots结果缓存到本地,并为每个节点创建唯一的JedisPool连接池。
3)JedisCluster执行键命令的过程有些复杂,但是理解这个过程对于开发人员分析定位问题非常有帮助。键命令执行流程:
计算slot并根据slots缓存获取目标节点连接,发送命令。
如果出现连接错误,使用随机连接重新执行键命令,每次命令重试对redi-rections参数减1.
捕获到MOVED重定向错误,使用cluster slots命令更新slots缓存(renewSlotCache方法)。
重复执行第一步和第三步,知道命令执行成功,或者当redirections<=0时抛出JedsiClusterMaxRedirectionsException异常。
从上面流程中发现,客户端需要结合异常和重试机制时刻保证跟Redis集群的slots同步,因此Smart客户端相比单机客户端有了很大的变化和实现难度。了解命令执行流程后,下面我们对Smart客户端成本和可能存在的问题进行分析:
1)客户端内部维护slots缓存表,并且针对每个节点维护连接池,当集群规模非常大时,客户端会维护非常多的连接并消耗更多的内存。
2)使用Jedis凑走集群是最常见的错误是:
throw new JedisClusterMaxRedirectionsExceptions("Too many Cluster redirections?");
这经常会引起开发人员的疑惑,它隐藏了内部错误细节,原因是节点宕机或请求超时都会抛出JedisConnectionException,导致触发了随机重试,当重试次数耗尽抛出这个错误。
3)当出现JedisConnectionException时,Jedis认为可能是集群节点故障需要随机重试来更新slots缓存,因此了解哪些异常将抛出JedisConnectionException变得非常重要,有如下几种情况会抛出JedisConnectionException:
前两点都可能是节点故障需要通过JedisConnectionException来更新slots缓存,但是第三点没有必要,因此Jedis2.8.1版本之后对于连接池的超时抛出JedisException,从而避免触发随机重试机制。
4)Redis集群支持自动故障转移,但是从故障发现到完成转移需要一定的时间,节点宕机期间所有指向这个节点的名都会触发随机重试,每次收到MOVED重定向后会调用JedisClusterInfoCache类的renewSlotCache方法。获得写锁后再执行cluster slots命令初始化缓存,由于集群所有的键命令都会执行getSlotPool方法方法计算槽对应节点,它内部要求读锁。ReentrantReadWriteLock是读锁共享且读写锁互斥,从而导致所有的请求都会造成阻塞。对于并发量高的场景将极大地影响集群吞吐。这个现象称为cluster slots风暴,有如下现象:
重试机制导致IO通信放大问题。比如默认重试5次的情况,当抛出JedisClusterMaxRedirectionsException异常时,内部最少需要9次IO通信:5次发送命令+2次ping命令保证随机节点正常+2次cluster slots命令初始化slots缓存。导致异常判定时间变长。
个别节点操作异常导致频繁的更新slots缓存,多次调用cluster slots命令,高并发是将过度消耗Redis节点资源,如果集群slot<->映射庞大则cluster slots返回信息越多,问题越严重。
频繁触发更新本地slots缓存操作,内部使用了写锁,阻塞对集群所有的键命令调用。
针对以上问题在Jedis2.8.2版本做了改进:
当接收到JedisConnectionException时不再轻易初始化slots缓存,大幅降低内部IO次数。逻辑为只有当重试次数到最后一次或者出现MovedDataException时才更新slots操作,降低了cluster slots命令代用次数。
当更新slots缓存时,不再使用ping命令检测节点活跃度,并且使用redis covering变量保证同一时刻只有一个线程更新slots缓存,其他线程忽略,优化了写锁阻塞和cluster slots调用次数。
综上所述,当出现JedisConnectionException时,命令发送次数变为5次:4次重试命令+1次cluster slots命令,同时避免了cluster slots不必要的并发调用。
开发提示:
Smart客户端——JedisCluster
(1)JedisCluster的定义
Jedis为Redis Cluster提供了Smart客户端,对应的类是JedisCluster,它的初始化方法如下:
public JedisCluster (Set<HostAndPort> jedisClusterNode, int connectionTiemout, int soTimeout, int maxAttempts, final GenericObjectPoolConfig poolConfig) {
...
}
其中包含了5个参数:
Set<HostAndPort> jedisClusterNode:所有Redis Cluster节点信息(也可以是一部分,因为客户端可以通过cluster slots自动发现)。
int connectionTimeout:连接超时。
int soTimeout:读写超时。
int maxAttempts:重试次数。
GenericObjectPoolConfig poolConfig:连接池参数,JedisCluster会为Redis Cluster的每个节点创建连接池。对于JedisCluster的使用需要注意以下几点:
JedisCluster包含了所有节点的连接池(JedisPool),所以建议JedisCluster使用单例。
JedisCluster每次操作完成后,不需要管理连接池的借还,它在内部已经完成。
JedisCluster一般不要执行close(),它会将所有JedisPool执行destroy操作。
(2)多节点命令和操作。
Redis Cluster虽然提供了分布式的特性,但是有些命令或者操作,诸如keys、flushall、删除 指定模式的键,需要遍历所有节点才可以完成。具体分为如下几个步骤:
通过jedisCluster.getClusterNodes()获取所有节点的连接池。
使用info replication筛选上一步中的主节点。
比那里主节点,使用scan命令找到指定模式的key,使用Pipeline机制删除。
(3)批量操作的方法
Redis Cluster中,由于key分布到各个节点上,会造成无法实现mget、mset等功能。但是可以利用CRC16算法计算出key对应的slot,以及Smart客户端保存了slot和节点对应关系的特性,将属于同一个Redis节点的key进行归档,然后分别对每个节点对应的子key列表执行mget或者pipeline操作。
(4)使用Lua、事务等特性的方法
Lua和事务需要所操作的key,必须在一个节点上,不过Redis Cluster提供了hashtag,如果开发人员确实需要使用Lua或者事务,可以将所要操作的key使用一个hashtag。具体操作步骤如下: