目前我们已经搭建好Redis集群并且理解了通信和伸缩细节, 但还没有使用客户端去操作集群。 Redis集群对客户端通信协议做了比较大的修改,为了追求性能最大化,并没有采用代理的方式而是采用客户端直连节点的方式。因此对于希望从单机切换到集群环境的应用需要修改客户端代码。本节我们关注集群请求路由的细节,以及客户端如何高效地操作集群。
10.5.1 请求重定向在集群模式下,Redis接收任何键相关命令时首先计算键对应的槽,再根据槽找出所对应的节点,如果节点是自身,则处理键命令;否则回复MOVED重定向错误,通知客户端请求正确的节点。这个过程称为MOVED重定向,如图10-29所示。
例如, 在之前搭建的集群上执行如下命令:
127.0.0.1:6379> set key:test:1 value-1
OK

执行set命令成功,因为键key:test:1对应槽5191正好位于6379节点负责的槽范围内,可以借助cluster keyslot{key}命令返回key所对应的槽,如下所示:
127.0.0.1:6379> cluster keyslot key:test:1
(integer) 5191
127.0.0.1:6379> cluster nodes
cfb28ef1deee4e0fa78da86abe5d24566744411e 127.0.0.1:6379 myself,master - 0 0 10 connected
1366-4095 4097-5461 12288-13652
...
再执行以下命令,由于键对应槽是9252,不属于6379节点,则回复MOVED{slot}{ip}{port}格式重定向信息:
127.0.0.1:6379> set key:test:2 value-2
(error) MOVED 9252 127.0.0.1:6380
127.0.0.1:6379> cluster keyslot key:test:2
(integer) 9252
重定向信息包含了键所对应的槽以及负责该槽的节点地址,根据这些信息客户端就可以向正确的节点发起请求。在6380节点上成功执行之前的命令:
127.0.0.1:6380> set key:test:2 value-2
OK
使用redis-cli命令时,可以加入-c参数支持自动重定向,简化手动发起重定向操作,如下所示:
#redis-cli -p 6379 -c
127.0.0.1:6379> set key:test:2 value-2
-> Redirected to slot [9252] located at 127.0.0.1:6380
OK
redis-cli自动帮我们连接到正确的节点执行命令, 这个过程是在redis-cli内部维护,实质上是client端接到MOVED信息之后再次发起请求,并不在Redis节点中完成请求转发,如图10-30所示。
节点对于不属于它的键命令只回复重定向响应,并不负责转发。熟悉Cassandra的用户希望在这里做好区分,不要混淆。正因为集群模式下把解析发起重定向的过程放到客户端完成,所以集群客户端协议相对于单机有了很大的变化。
键命令执行步骤主要分两步:计算槽,查找槽所对应的节点。下面分别介绍。
1.计算槽
Redis首先需要计算键所对应的槽。根据键的有效部分使用CRC16函数计算出散列值,再取对16383的余数,使每个键都可以映射到0~16383槽范围内。伪代码如下:
def key_hash_slot(key):
int keylen = key.length();
for (s = 0; s < keylen; s++):
if (key[s] == '{'):
break;
if (s == keylen) return crc16(key,keylen) & 16383;
for (e = s+1; e < keylen; e++):
if (key[e] == '}') break;
if (e == keylen || e == s+1) return crc16(key,keylen) & 16383;
/* 使用{和}之间的有效部分计算槽 */
return crc16(key+s+1,e-s-1) & 16383;

根据伪代码, 如果键内容包含{和}大括号字符, 则计算槽的有效部分是括号内的内容;否则采用键的全内容计算槽。
cluster keyslot命令就是采用key_hash_slot函数实现的,例如:
127.0.0.1:6379> cluster keyslot key:test:111
(integer) 10050
127.0.0.1:6379> cluster keyslot key:{hash_tag}:111
(integer) 2515
127.0.0.1:6379> cluster keyslot key:{hash_tag}:222
(integer) 2515
其中键内部使用大括号包含的内容又叫做hash_tag,它提供不同的键可以具备相同slot的功能,常用于Redis IO优化。
例如在集群模式下使用mget等命令优化批量调用时,键列表必须具有相同的slot,否则会报错。这时可以利用hash_tag让不同的键具有相同的slot达到优化的目的。命令如下:
127.0.0.1:6385> mget user:10086:frends user:10086:videos
(error) CROSSSLOT Keys in request don't hash to the same slot
127.0.0.1:6385> mget user:{10086}:friends user:{10086}:videos
1) "friends"
2) "videos"
开发提示Pipeline同样可以受益于hash_tag,由于Pipeline只能向一个节点批量发送执行命令,而相同slot必然会对应到唯一的节点,降低了集群使用Pipeline的门槛。
2.槽节点查找Redis计算得到键对应的槽后,需要查找槽所对应的节点。集群内通过消息交换每个节点都会知道所有节点的槽信息,内部保存在clusterState结构中,结构所示:
typedef struct clusterState {
clusterNode *myself; /* 自身节点,clusterNode代表节点结构体 */
clusterNode *slots[CLUSTER_SLOTS]; /* 16384个槽和节点映射数组, 数组下标代表对应的槽 */
...
} clusterState;
slots数组表示槽和节点对应关系,实现请求重定向伪代码如下:
def execute_or_redirect(key):
int slot = key_hash_slot(key);
ClusterNode node = slots[slot];
if(node == clusterState.myself):
return executeCommand(key);
else:
return '(error) MOVED {slot} {node.ip}:{node.port}';
根据伪代码看出节点对于判定键命令是执行还是MOVED重定向, 都是借助slots[CLUSTER_SLOTS]数组实现。根据MOVED重定向机制,客户端可以随机连接集群内任一Redis获取键所在节点,这种客户端又叫Dummy(傀儡)客户端,它优点是代码实现简单,对客户端协议影响较小,只需要根据重定向信息再次发送请求即可。但是它的弊端很明显,每次执行键命令前都要到Redis上进行重定向才能找到要执行命令的节点,额外增加了IO开销,这不是Redis集群高效的使用方式。正因为如此通常集群客户端都采用另一种实现:Smart(智能)客户端。
10.5.2 Smart客户端
1.smart客户端原理大多数开发语言的Redis客户端都采用Smart客户端支持集群协议,客户端如何选择见:http://redis.io/clients,从中找出符合自己要求的客户端类库。Smart客户端通过在内部维护slot→node的映射关系,本地就可实现键到节点的查找,从而保证IO效率的最大化,而MOVED重定向负责协助Smart客户端更新slot→node映射。我们以Java的Jedis为例,说明Smart客户端操作集群的流程。
1)首先在JedisCluster初始化时会选择一个运行节点,初始化槽和节点映射关系,使用cluster slots命令完成 如下所示:
public class JedisClusterInfoCache {
private Map
private Map
...
}
3)JedisCluster执行键命令的过程有些复杂,但是理解这个过程对于开发人员分析定位问题非常有帮助,部分代码如下:
public abstract class JedisClusterCommand
// 集群节点连接处理器
private JedisClusterConnectionHandler connectionHandler;
// 重试次数, 默认5次
private int redirections;
// 模板回调方法
public abstract T execute(Jedis connection);
public T run(String key) {
if (key == null) {
throw new JedisClusterException("No way to dispatch this command to Redis Cluster.");
}
return runWithRetries(SafeEncoder.encode(key), this.redirections, false, false);
}
// 利用重试机制运行键命令
private T runWithRetries(byte[] key, int redirections, boolean tryRandomNode, boolean asking) {
if (redirections <= 0) {
throw new JedisClusterMaxRedirectionsException("Too many Cluster redirections");
}
Jedis connection = null;
try {
if (tryRandomNode) {
// 随机获取活跃节点连接
connection = connectionHandler.getConnection();
} else {
// 使用slot缓存获取目标节点连接
connection = connectionHandler.getConnectionFromSlot(JedisClusterCRC16.getSlot(key));
}
return execute(connection);
} catch (JedisConnectionException jce) {
// 出现连接错误使用随机连接重试
return runWithRetries(key, redirections - 1, true/*开启随机连接*/, asking);
} catch (JedisRedirectionException jre) {
if (jre instanceof JedisMovedDataException) {
// 如果出现MOVED重定向错误,在连接上执行cluster slots命令重新初始化slot缓存
this.connectionHandler.renewSlotCache(connection);
}
// slot初始化后重试执行命令
return runWithRetries(key, redirections - 1, false, asking);
} finally {
releaseConnection(connection);
}
}
}
键命令执行流程:
1)计算slot并根据slots缓存获取目标节点连接,发送命令。
2)如果出现连接错误, 使用随机连接重新执行键命令, 每次命令重试对redi-rections参数减1。
3)捕获到MOVED重定向错误, 使用cluster slots命令更新slots缓存(renewSlotCache方法)。
4)重复执行1)~3)步,直到命令执行成功,或者当redirections<=0时抛出Jedis ClusterMaxRedirectionsException异常。
整个流程如图10-31所示。

从命令执行流程中发现,客户端需要结合异常和重试机制时刻保证跟Redis集群的slots同步,因此Smart客户端相比单机客户端有了很大的变化和实现难度。了解命令执行流程后,下面我们对Smart客户端成本和可能存在的问题进行分析:
1)客户端内部维护slots缓存表,并且针对每个节点维护连接池,当集群规模非常大时,客户端会维护非常多的连接并消耗更多的内存。
2)使用Jedis操作集群时最常见的错误是:throw new JedisClusterMaxRedirectionsException("Too many Cluster redirections");这经常会引起开发人员的疑惑,它隐藏了内部错误细节,原因是节点宕机或请求超时都会抛出JedisConnectionException,导致触发了随机重试,当重试次数耗尽抛出这个错误。
3)当出现JedisConnectionException时,Jedis认为可能是集群节点故障需要随机重试来更新slots缓存,因此了解哪些异常将抛出JedisConnectionException变得非常重要,有如下几种情况会抛出JedisConnectionException:
·Jedis连接节点发生socket错误时抛出。
·所有命令/Lua脚本读写超时抛出。
·JedisPool连接池获取可用Jedis对象超时抛出。
前两点都可能是节点故障需要通过JedisConnectionException来更新slots缓存,但是第三点没有必要,因此Jedis2.8.1版本之后对于连接池的超时抛出Jedis Exception,从而避免触发随机重试机制。
4)Redis集群支持自动故障转移,但是从故障发现到完成转移需要一定的时间,节点宕机期间所有指向这个节点的命令都会触发随机重试,每次收到MOVED重定向后会调用JedisClusterInfoCache类的renewSlotCache方法。部分代码如下:
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public void renewSlotCache(Jedis jedis) {
try {
cache.discoverClusterSlots(jedis);
} catch (JedisConnectionException e) {
renewSlotCache();
}
}
public void discoverClusterSlots(Jedis jedis) {
// 获取写锁
w.lock();
try {
this.slots.clear();
// 执行cluster slots
List
评论记录:
回复评论: