Redis通过对KEY计算hash,将KEY映射到slot,集群中每个节点负责一部分slot的方式管理数据,slot最大个数为16384。
在集群节点对应的结构体变量clusterNode中可以看到slots数组,数组的大小为CLUSTER_SLOTS除以8,CLUSTER_SLOTS的值是16384:
1 |
|
因为一个字符占8位,所以数组个数为16384除以8,每一位可以表示一个slot,如果某一位的值为1,表示当前节点负责这一位对应的slot。
clusterState
clusterNode里面保存了节点相关的信息,集群数据迁移信息并未保存在clusterNode中,而是使用了clusterState结构体来保存:
- migrating_slots_to数组: 记录当前节点负责的slot迁移到了哪个节点
- importing_slots_from数组: 记录当前节点负责的slot是从哪个节点迁入的
- slots数组:记录每个slot是由哪个集群节点负责的
- slots_keys_count:slot中key的数量
- slots_to_keys:是一个字典树,记录KEY和SLOT的对应关系
1 | typedef struct clusterState { |
clusterState与clusterNode的关系
集群数据迁移
在手动进行数据迁移时,需要执行以下步骤:
- 在源节点和目标节点分别使用
CLUSTER SETSLOT MIGRATING
和CLUSTER SETSLOT IMPORTING
标记slot迁出和迁入信息 - 在源节点使用
CLUSTER GETKEYSINSLOT
命令获取待迁出的KEY - 在源节点执行
MIGRATE
命令进行数据迁移,MIGRATE
既支持单个KEY的迁移,也支持多个KEY的迁移 - 在源节点和目标节点使用
CLUSTER SETSLOT
命令标记slot最终迁移节点
标记数据迁移节点
在进行数据迁移之前,首先在需要迁入的目标节点使用SETSLOT
命令标记要将SLOT从哪个节点迁入到当前节点:
:哈希槽的值 :表示slot所在的节点
1 | CLUSTER SETSLOT <slot> IMPORTING <node> |
然后在源节点也就是slot所在节点使用MIGRATING
命令将数据迁出到目标节点:
:哈希槽的值 :表示slot要迁出到的目标节点
1 | CLUSTER SETSLOT <slot> MIGRATING <node> |
比如slot1当前在node1中,需要将slot1迁出到node2,那么首先在nodd2上执行IMPORTING
命令,标记slot准备从node1迁到当前节点node2中:
1 | CLUSTER SETSLOT slot1 IMPORTING node1 |
然后在node1中执行MIGRATING
命令标记slot1需要迁移到node2:
1 | CLUSTER SETSLOT slot1 MIGRATING node2 |
clusterCommandSETSLOT
命令的处理在clusterCommand函数(cluster.c文件中)中:
- 校验当前节点是否是从节点,如果当前节点是从节点,返回错误,
SETSLOT
只能用于主节点 - 如果是
migrating
命令,表示slot需要从当前节点迁出到其他节点,处理如下:
(1) 如果需要迁移的slot不在当前节点,返回错误
(2)如果要迁移到的目标slot节点未查询到,返回错误
(3)将当前节点的migrating_slots_to[slot]的值置为迁出到的目标节点,记录slot迁移到了哪个节点 - 如果是
importing
命令,表示slot需要从其他节点迁入到当前节点
(1)如果要迁移的slot已经在当前节点,返回slot数据已经在当前节点的响应
(2)由于importing需要从slot所在节点迁移到当前节点,如果未从集群中查询slot当前所在节点,返回错误信息
(3)将当前节点的importing_slots_from[slot]置为slot所在节点,记录slot是从哪个节点迁入到当前节点的
1 | void clusterCommand(client *c) { |
获取待迁出的key
在标记完迁入、迁出节点后,就可以使用CLUSTER GETKEYSINSLOT
命令获取待迁出的KEY:
1 | CLUSTER GETKEYSINSLOT <slot> <count> |
getkeysinslot
命令的处理也在clusterCommand函数中,处理逻辑如下:
- 从命令中解析slot的值以及count的值,count的值记为maxkeys,并校验合法性
- 调用countKeysInSlot函数获取slot中key的数量,与maxkeys对比,如果小于maxkeys,就将maxkeys的值更新为slot中key的数量
- 根据获取key的个数分配相应的内存空间
- 从slot中获取key并将数据返回给客户端
1 | void clusterCommand(client *c) { |
数据迁移
源节点数据迁移
完成上两步之后,接下来需要在源节点中执行MIGRATE
命令进行数据迁移,MIGRATE
既支持单个KEY的迁移,也支持多个KEY的迁移,语法如下:
1 | 单个KEY |
- host:ip地址
- Port:端口
- key:迁移的key
- KEYS:如果一次迁移多个KEY,使用KEYS,后跟迁移的key1 … keyN
- dbid:数据库id
- COPY:如果目标节点已经存在迁移的key,则报错,如果目标节点不存在迁移的key,则正常进行迁移,在迁移完成后删除源节点中的key
- REPLACE:如果目标节点不存在迁移的key,正常进行迁移,如果目标节点存在迁移的key,进行替换,覆盖目标节点中已经存在的key
- AUTH:验证密码
migrateCommand
MIGRATE
命令对应的处理函数在migrateCommand中(cluster.c文件中),处理逻辑如下:
- 解析命令中的参数,判断是否有replace、auth、keys等参数
- 如果有replace参数,表示在迁移数据时如果key已经在目标节点存在,进行替换
- 如果有keys参数,表示命令中有多个key,计算命令中key的个数记为num_keys
- 处理命令中解析到的所有key,调用lookupKeyRead函数查找key:
- 如果查找到,将key放入kv对象中,kv中存储实际要处理的KEY,value放入ov对象中,ov中存储key对应的value
- 如果未查找到key,跳过当前key,处理下一个key
- 因为有部分key可能未查询到,所以更新实际需要处理的key的数量num_keys
- 根据命令中的ip端口信息,与目标节点建立连接
- 调用rioInitWithBuffer函数初始化一块缓冲区
- 处理实际需要迁移的key,主要是将数据填入缓冲区
- 根据key获取过期时间,如果已过期不进行处理
- 判断是否开启了集群,如果开启了集群将
RESTORE-ASKING
写入缓冲区,如果未开启,写入RESTORE命令 - 将key写入缓冲区
- 调用createDumpPayload函数,创建payload,将RDB版本、CRC64校验和以及value内容写入 ,目标节点收到数据时需要进行校验
- 将payload数据填充到缓冲区
- 将缓冲区的数据按照64K的块大小发送到目标节点
1 | void migrateCommand(client *c) { |
createDumpPayload
createDumpPayload函数在cluster.c文件中:
1 | /* ----------------------------------------------------------------------------- |
目标节点处理数据
restoreCommand
目标节点收到迁移的数据的处理逻辑在restoreCommand中(cluster.c文件中):
- 解析请求中的参数,判断是否有replace
- 如果没有replace并且key已经在当前节点存在,返回错误信息
- 调用verifyDumpPayload函数校验RDB版本和CRC校验和
- 从请求中解析value的数据类型和value值
- 如果设置了
replace
先删除数据库中存在的key - 将key和vlaue添加到节点的数据库中
1 | /* RESTORE key ttl serialized-value [REPLACE] */ |
标记迁移结果
数据迁移的最后一步,需要使用CLUSTER SETSLOT
命令,在源节点和目标节点执行以下命令,标记slot最终所属的节点,并清除第一步中标记的迁移信息:
1 | CLUSTER SETSLOT <slot> NODE <node> |
clusterCommand
CLUSTER SETSLOT <slot> NODE <node>
命令的处理依旧在clusterCommand函数中,处理逻辑如下:
- 根据命令中传入的nodeid查找节点记为n,如果未查询到,返回错误信息
- 果slot已经在当前节点,但是根据nodeid查找到的节点n不是当前节点,说明slot所属节点与命令中指定的节点不一致,返回错误信息
- 在源节点上执行命令时,如果slot中key的数量为0,表示slot上的数据都已迁移完毕,而migrating_slots_to[slot]记录了slot迁移到的目标节点,既然数据已经迁移完成此时需要将migrating_slots_to[slot]迁出信息清除
- 调用clusterDelSlot函数先将slot删除
- 获取slot所属节点
- 将slot所属节点ClusterNode结构体中的slots数组对应的标记位取消,表示节点不再负责此slot
- 将slot所属节点ClusterState结构体中的slots数组对应元素置为NULL,表示当前slot所属节点为空
- 调用clusterAddSlot将slot添加到最终所属的节点中
- 在目标节点上执行命令时,如果slot所属节点为当前节点,并且importing_slots_from[slot]不为空, importing_slots_from[slot]中记录了slot是从哪个节点迁移过来,此时数据已经迁移完毕,清除 importing_slots_from[slot]中的迁入信息
1 | void clusterCommand(client *c) { |
参考
Redis版本:redis-6.2.5