Redis集群是Redis的分布式实现,具有高性能和线性可扩展性,最多可拥有1000个节点;具有可接受的写入安全程度,系统尽力保留与大多主节点连接的客户端的所有写入;同时保持可用性,集群能够在大多数主节点可用区中存活。集群通过分片的方式进行数据共享,并提供复制和故障转移功能。
1. 集群节点
Redis集群通常包含多个节点,但最开始时,每个节点都是独立的集群,要将它们连接在一起需要执行CLUSTER MEET <ip> <port>
命令,让指定的节点与之握手,一旦握手成功,就会将该节点加入当前节点所在集群中。
1.1 集群数据结构
clusterNode结构保存节点当前状态,每个节点都会使用该结构为自己创建一个状态,并且也会为集群中其他节点创建相应的状态。
1 | typedef struct clusterNode { |
clusterLink结构是存储连接该节点所需的有关信息状态
1 | typedef struct clusterLink { |
redisClient和clusterLink都有自己的socket、文件描述符和输入输出缓冲区,不同的是redisClient用于保存客户端的连接,而clusterLink用于保存节点间的连接
每个节点都有一个clusterState结构,该结构记录了当前纪元集群的状态
1 | typedef struct clusterState { |
1.2 CLUSTER MEET
命令
收到命令的节点A将于指定地址端口的节点B进行握手:
- 节点A为节点B创建clusterNode结构,保存B的状态信息,并添加到自己的
clusterState.nodes
中 - 节点A向节点B发送MEET消息
- 节点B接收到A的MEET消息,会为A创建clusterNode结构,同样保存到自己的
clusterState.nodes
中 - 节点B返回PONG消息给节点A
- 节点A接收到PONG消息,表示与B节点MEET成功
- 节点A发送PING给节点B
- 节点B收到节点A的PING消息,表示节点A已经收到了自己的PONG
2. 散列槽指派
Redis集群通过分片的方式保存键值对,集群中整个数据库被分为16384个slot,通过HASH_SLOT = CRC16(key) mod 16384
的方式确定某个键存储在哪个散列槽中。
1 | typedef struct clusterNode { |
slots是一个二进制位数组,长度为16384/8=2048个字节,包含16384个二进制位,刚好对应每个散列槽。如果节点的slots数组的第i位为1,表示该节点负责这个散列槽数据的处理。numslots表示当前节点处理散列槽的数量。
每个节点除了负责字节要处理的散列槽之外,还需要将自己slots数组告知集群的其他节点,当节点接收到来自其他节点的信息后,会更新自己保存的状态数据。
1 | typedef struct clusterState { |
clusterState结构中slots记录了集群所有散列槽的指派信息,slots数组包含16384个项,每个都是指向集群节点的指针。如果slots[i]为NULL,表示该散列槽尚未指派给任何节点,否则指向处理给散列槽的节点。
3. 重定向和重新分片
3.1 MOVED错误
在对数据的16384个散列槽都进行了指派后,集群就会进入上线状态,这时Redis客户端可以自由的向集群中每个节点发送查询,当命令与数据库键有关时,节点会计算该键属于哪个散列槽,并检查该散列槽是否指派给自己,如果该散列槽并未指派给自己,那么节点会返回一个MOVED错误,告知客户端重定向到正确的节点。
MOVED错误的格式为MOVED <slot> <ip>:<port>
,当客户端接收到节点返回的MOVED错误信息,会根据其提供的IP和端口号访问该节点,重新执行之前的命令。
3.2 重新分片
Redis集群的重新分片可以修改散列槽的指派,并且对应槽的键也会修改到相应节点。该操作可以在线进行,重新分片过程中,集群不需要下线,而且还可以继续处理请求。
Redis集群重新分片由redis-trib
负责执行,它对集群某单个散列槽重新分片步骤如下:
redis-trib
向目标节点发送CLUSTER SETSLOT <slot> IMPORTING <source_id>
命令,让目标节点准备好从源节点导入指定散列槽的键值对。redis-trib
向源节点发送CLUSTER SETSLOT <slot> MIGRATING <target_id>
命令,让源节点准备好将指定散列槽的键值对迁移至目标节点redis-trib
向源节点发送CLUSTER GETKEYSINLSOT <slot> <count>
命令,获得最多count个属于slot的键值对的键- 对于3获得的每个key,
redis-trib
向源节点发送MIGRATE <target_ip> <target_port> <key_name> 0 <timeout>
,将键原子的从源节点迁移到目标节点 - 重复执行步骤3、4,直到指定散列槽所有键值对迁移完成
redis-trib
向集群中任一节点发送CLUSTER SETSLOT <slot> NODE <target_id>
,将散列槽slot指派给目标节点,这一指派信息会发送给整个集群
3.3 ASK错误
节点接收到有关key的命令,若为在自己数据库内查到该key,则检查自己的clusterState.migrating_slots_to[i]
,如果该key正在进行迁移,那么节点回复客户端一个ASK错误,指引客户端在正确位置寻找key。
3.4 ASKING
客户端请求集群中节点关于键key的命令,若该节点并未指派key所在的散列槽,但节点的clusterState.importing_slots_from[i]
表示正在导入散列槽i,并且发送命令客户端带有REDIS_ASKING
标识,那么节点将执行关于该散列槽的操作一次。
当客户端接收到ASK错误并转向正在导入散列槽的节点时,先向节点发送ASKING命令,然后重新发送要执行的命令,因为不发生ASKING命令而直接发送执行命令,会被节点拒绝,并返回MOVED错误。
3.5 MOVED错误和ASK错误的区别
- MOVED错误表示散列槽的负责权已经从一个节点转移到另一个节点,当客户端遇到MOVED错误,只需将目标节点换成MOVED重定向的节点
- ASK错误是两个节点散列槽迁移过程中的临时措施,客户端收到ASK错误后,会在接下来的命令请求换成ASK错误中指定的节点,但之后并不会改变,客户端仍然请求源节点