已经有了管理主从集群的哨兵,为什么还需要推出切片集群呢?我认为有两个比较重要的原因:
N/2+1
下面来聊聊 Redis cluster 是如何解决这两个问题的。
切片集群是一种水平扩展的技术方案,它的主体思想是增加 Redis 实例组成集群,将原来保存在单个实例的上数据切片按照某种算法分散在各个不同的实例上,以减轻单个实例数据过大时同步和持久化时的压力。同时,水平扩展方案和垂直扩展方案相比扩展性更强,受硬件和成本的影响更小。
目前主要的实现方案有官方的 Redis Cluster 和 第三方的 Codis。Codis 在官方的 Redis Cluster 成熟之后已经很久没有更新了,这里就不特别介绍了,主要介绍下 Redis Cluster 的实现。
切片集群实际就是一个分布式的数据库,而分布式数据库首先要解决的问题就是如何把整个数据集划分到多个节点上。
Redis Cluster 采用的是虚拟槽的分区技术。Redis Cluster 一共定义了 16384 个槽,数据与槽关联,而不是和实际的节点关联,这样可以很好的将数据和节点解耦,方便数据拆分和集群扩展。通常情况下,Redis 会将槽平均分配到节点上,用户可以使用命令 cluster addslots 来手动分配。需要注意的是,手动分配哈希槽时必须要把 16384 个槽都分配完,否则 Redis 集群会无法工作。
映射到节点的流程有两步:
集群功能目前有一些功能限制:
为了解决上述的第一个问题,Redis Cluster 提供了哈希标签(hash tag)功能,例如有两条 Redis 命令:
set Hello world
set Hello1 world
这两个操作的 key 可能不会落在同一个槽上。这时候如果将 Hello1 改成 {Hello}1,Redis就会只计算被{}包围的字符串属于那个槽,这样这两个命令的 key 就会落在同一个槽上,就可以使用 mset、事务、lua 脚本来处理了。
可以使用命令 cluster keyslot
来验证两个 key 是否落在同一个槽上。
127.0.0.1:30001> cluster keyslot hello
(integer) 866
127.0.0.1:30001> cluster keyslot hello1
(integer) 11613
127.0.0.1:30001> cluster keyslot {hello}1
(integer) 866
如果没有使用哈希标签,如果 Redis 命令中 key 计算之后的哈希值不是落在当前节点持有的槽内,节点会返回一个 MOVED 错误,告诉客户端该操作应该在哪个节点上执行:
127.0.0.1:30006> set hello1 world
(error) MOVED 11613 127.0.0.1:30003
127.0.0.1:30006>
节点是不会处理请求转发的功能,我们启动 redis-cli 的时候可以添加一个 -c 参数,这样,redis-cli 就会帮我们转发请求了,而不是返回一个错误:
$ redis-cli -p 30006 -c
127.0.0.1:30006> set hello1 world
-> Redirected to slot [11613] located at 127.0.0.1:30003
OK
当集群进行伸缩重新分配槽的时候,如果有请求需要处理落在迁移中的槽上,那么 Redis Cluster 会怎么处理呢?
Redis 定义了一个结构 clusterState
来记录本地的集群状态,其中有几个成员来记录槽的信息:
typedef struct clusterState {
...
clusterNode *migrating_slots_to[CLUSTER_SLOTS]; //记录槽转移的目标节点
clusterNode *importing_slots_from[CLUSTER_SLOTS]; //记录槽转移的源节点
clusterNode *slots[CLUSTER_SLOTS]; //记录集群中槽所属的节点
...
} clusterState;
当开始重新分配槽时,拥有槽的原节点会将目标节点记录到 migrating_slots_to
中,目标节点会将原节点信息记录到 importing_slots_from
中,重分配槽的过程中,槽的拥有者还是原节点。
此时原节点收到操作命令时,如果在本地找不到数据,会在 migrating_slots_to
中找到目标节点信息,然后返回 ACK 重定向来告诉客户端对应的数据正在迁移到目标节点。
$ redis-cli -p 6380 -c get key:test:5028
(error) ASK 4096 127.0.0.1:6380
收到 ACK 之后,不像 MOVED 错误一样直接到对应的节点上执行命令就可以了,首先需要发送一个 ASKING,然后再发送实际的命令。这是因为在重分配槽的过程中,槽的所有者还没有发生改变,如果直接向目标节点发送命令,目标节点会直接返回 MOVED 错误,因为目标节点在本地的 clusterState→slots
中并没有发现 key 所属的槽分配给了自己。
需要注意的是 ASKING 命令是临时的,收到 AKSING 命令后会开启 CLIENT_ASKING(askingCommand 函数),执行完命令后会将 CLIENT_ASKING 状态清除(resetClient函数)。
在搭建好的集群中,插入 3 条数据:
mset key:test:5028 world key:test:68253 world key:test:792