Redis通过对KEY计算hash,将KEY映射到slot,集群中每个节点负责一部分slot的方式管理数据,slot最大个数为16384。

在集群节点对应的结构体变量clusterNode中可以看到slots数组,数组的大小为CLUSTER_SLOTS除以8,CLUSTER_SLOTS的值是16384:

  1. #define CLUSTER_SLOTS 16384
  2. typedef struct clusterNode {
  3. unsigned char slots[CLUSTER_SLOTS/8];
  4. // 省略...
  5. } clusterNode;

因为一个字符占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 {
  2. clusterNode *myself; /* 当前节点自己 */
  3. clusterNode *migrating_slots_to[CLUSTER_SLOTS];
  4. clusterNode *importing_slots_from[CLUSTER_SLOTS];
  5. clusterNode *slots[CLUSTER_SLOTS];
  6. uint64_t slots_keys_count[CLUSTER_SLOTS];
  7. rax *slots_to_keys;
  8. // ...
  9. } clusterState;

clusterState与clusterNode的关系

集群数据迁移

在手动进行数据迁移时,需要执行以下步骤:

  1. 在源节点和目标节点分别使用CLUSTER SETSLOT MIGRATINGCLUSTER SETSLOT IMPORTING标记slot迁出和迁入信息
  2. 在源节点使用CLUSTER GETKEYSINSLOT 命令获取待迁出的KEY
  3. 在源节点执行MIGRATE命令进行数据迁移,MIGRATE既支持单个KEY的迁移,也支持多个KEY的迁移
  4. 在源节点和目标节点使用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

clusterCommand

SETSLOT命令的处理在clusterCommand函数(cluster.c文件中)中:

  1. 校验当前节点是否是从节点,如果当前节点是从节点,返回错误,SETSLOT只能用于主节点
  2. 如果是migrating命令,表示slot需要从当前节点迁出到其他节点,处理如下:

    (1) 如果需要迁移的slot不在当前节点,返回错误

    (2)如果要迁移到的目标slot节点未查询到,返回错误

    (3)将当前节点的migrating_slots_to[slot]的值置为迁出到的目标节点,记录slot迁移到了哪个节点
  3. 如果是importing命令,表示slot需要从其他节点迁入到当前节点

    (1)如果要迁移的slot已经在当前节点,返回slot数据已经在当前节点的响应

    (2)由于importing需要从slot所在节点迁移到当前节点,如果未从集群中查询slot当前所在节点,返回错误信息

    (3)将当前节点的importing_slots_from[slot]置为slot所在节点,记录slot是从哪个节点迁入到当前节点的
  1. void clusterCommand(client *c) {
  2. if (server.cluster_enabled == 0) {
  3. addReplyError(c,"This instance has cluster support disabled");
  4. return;
  5. }
  6. if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
  7. // ...
  8. }
  9. // ...
  10. else if (!strcasecmp(c->argv[1]->ptr,"setslot") && c->argc >= 4) { // 处理setslot命令
  11. int slot;
  12. clusterNode *n;
  13. // 如果当前节点是从节点,返回错误,SETSLOT只能用于主节点
  14. if (nodeIsSlave(myself)) {
  15. addReplyError(c,"Please use SETSLOT only with masters.");
  16. return;
  17. }
  18. // 查询slot
  19. if ((slot = getSlotOrReply(c,c->argv[2])) == -1) return;
  20. // 处理migrating迁出
  21. if (!strcasecmp(c->argv[3]->ptr,"migrating") && c->argc == 5) {
  22. // 如果需要迁移的slot不在当前节点,返回错误
  23. if (server.cluster->slots[slot] != myself) {
  24. addReplyErrorFormat(c,"I'm not the owner of hash slot %u",slot);
  25. return;
  26. }
  27. // 如果要迁移到的目标节点未查询到,返回错误
  28. if ((n = clusterLookupNode(c->argv[4]->ptr)) == NULL) {
  29. addReplyErrorFormat(c,"I don't know about node %s",
  30. (char*)c->argv[4]->ptr);
  31. return;
  32. }
  33. // 将当前节点的migrating_slots_to[slot]置为目标节点,记录slot要迁移到的节点
  34. server.cluster->migrating_slots_to[slot] = n;
  35. } else if (!strcasecmp(c->argv[3]->ptr,"importing") && c->argc == 5) { // 处理importing迁入
  36. // 如果要迁移的slot已经在当前节点
  37. if (server.cluster->slots[slot] == myself) {
  38. addReplyErrorFormat(c,
  39. "I'm already the owner of hash slot %u",slot);
  40. return;
  41. }
  42. // importing需要从slot所在节点迁移到当前节点,如果未从集群中查询slot当前所在节点,返回错误信息
  43. if ((n = clusterLookupNode(c->argv[4]->ptr)) == NULL) {
  44. addReplyErrorFormat(c,"I don't know about node %s",
  45. (char*)c->argv[4]->ptr);
  46. return;
  47. }
  48. // 记录slot是从哪个节点迁移过来的
  49. server.cluster->importing_slots_from[slot] = n;
  50. }
  51. // 省略其他if else
  52. // ...
  53. else {
  54. addReplyError(c,
  55. "Invalid CLUSTER SETSLOT action or number of arguments. Try CLUSTER HELP");
  56. return;
  57. }
  58. clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|CLUSTER_TODO_UPDATE_STATE);
  59. addReply(c,shared.ok);
  60. }
  61. // ...
  62. else {
  63. addReplySubcommandSyntaxError(c);
  64. return;
  65. }
  66. }

获取待迁出的key

在标记完迁入、迁出节点后,就可以使用CLUSTER GETKEYSINSLOT 命令获取待迁出的KEY:

:哈希槽的值

:迁出KEY的数量

  1. CLUSTER GETKEYSINSLOT <slot> <count>

getkeysinslot命令的处理也在clusterCommand函数中,处理逻辑如下:

  1. 从命令中解析slot的值以及count的值,count的值记为maxkeys,并校验合法性
  2. 调用countKeysInSlot函数获取slot中key的数量,与maxkeys对比,如果小于maxkeys,就将maxkeys的值更新为slot中key的数量
  3. 根据获取key的个数分配相应的内存空间
  4. 从slot中获取key并将数据返回给客户端
  1. void clusterCommand(client *c) {
  2. if (server.cluster_enabled == 0) {
  3. addReplyError(c,"This instance has cluster support disabled");
  4. return;
  5. }
  6. if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
  7. // ...
  8. }
  9. // ...
  10. else if (!strcasecmp(c->argv[1]->ptr,"getkeysinslot") && c->argc == 4) {
  11. /* CLUSTER GETKEYSINSLOT <slot> <count> */
  12. long long maxkeys, slot;
  13. unsigned int numkeys, j;
  14. robj **keys;
  15. // 从命令中获取slot的值并转为长整型
  16. if (getLongLongFromObjectOrReply(c,c->argv[2],&slot,NULL) != C_OK)
  17. return;
  18. // 从命令中获取key的最大个数并转为长整型
  19. if (getLongLongFromObjectOrReply(c,c->argv[3],&maxkeys,NULL)
  20. != C_OK)
  21. return;
  22. // 如果slot的值小于0或者大于CLUSTER_SLOTS或者key的最大个数为0
  23. if (slot < 0 || slot >= CLUSTER_SLOTS || maxkeys < 0) {
  24. addReplyError(c,"Invalid slot or number of keys");
  25. return;
  26. }
  27. // 计算slot中key的数量
  28. unsigned int keys_in_slot = countKeysInSlot(slot);
  29. // 如果maxkeys大于slot中key的数量,更新maxkeys的值为slot中key的数量
  30. if (maxkeys > keys_in_slot) maxkeys = keys_in_slot;
  31. // 分配空间
  32. keys = zmalloc(sizeof(robj*)*maxkeys);
  33. // 从slot中获取key
  34. numkeys = getKeysInSlot(slot, keys, maxkeys);
  35. addReplyArrayLen(c,numkeys);
  36. for (j = 0; j < numkeys; j++) {
  37. // 返回key
  38. addReplyBulk(c,keys[j]);
  39. decrRefCount(keys[j]);
  40. }
  41. zfree(keys);
  42. }
  43. // ...
  44. else {
  45. addReplySubcommandSyntaxError(c);
  46. return;
  47. }
  48. }

数据迁移

源节点数据迁移

完成上两步之后,接下来需要在源节点中执行MIGRATE命令进行数据迁移,MIGRATE既支持单个KEY的迁移,也支持多个KEY的迁移,语法如下:

  1. # 单个KEY
  2. MIGRATE host port key dbid timeout [COPY | REPLACE | AUTH password | AUTH2 username password]
  3. # 多个KEY
  4. MIGRATE host port "" dbid timeout [COPY | REPLACE | AUTH password | AUTH2 username password] KEYS key2 ... keyN
  • 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文件中),处理逻辑如下:

  1. 解析命令中的参数,判断是否有replace、auth、keys等参数

    • 如果有replace参数,表示在迁移数据时如果key已经在目标节点存在,进行替换
    • 如果有keys参数,表示命令中有多个key,计算命令中key的个数记为num_keys
  2. 处理命令中解析到的所有key,调用lookupKeyRead函数查找key:
    • 如果查找到,将key放入kv对象中,kv中存储实际要处理的KEY,value放入ov对象中,ov中存储key对应的value
    • 如果未查找到key,跳过当前key,处理下一个key
  3. 因为有部分key可能未查询到,所以更新实际需要处理的key的数量num_keys
  4. 根据命令中的ip端口信息,与目标节点建立连接
  5. 调用rioInitWithBuffer函数初始化一块缓冲区
  6. 处理实际需要迁移的key,主要是将数据填入缓冲区
    • 根据key获取过期时间,如果已过期不进行处理
    • 判断是否开启了集群,如果开启了集群将RESTORE-ASKING写入缓冲区,如果未开启,写入RESTORE命令
    • 将key写入缓冲区
    • **调用createDumpPayload函数,创建payload,将RDB版本、CRC64校验和以及value内容写入 **,目标节点收到数据时需要进行校验
    • 将payload数据填充到缓冲区
  7. 将缓冲区的数据按照64K的块大小发送到目标节点
  1. void migrateCommand(client *c) {
  2. // 省略...
  3. robj **ov = NULL; /* 保存要迁移的key对应的value */
  4. robj **kv = NULL; /* 保存要迁移的key. */
  5. int first_key = 3; /* 第一个key */
  6. int num_keys = 1; /* 迁移key的数量 */
  7. /* 解析命令中的参数 */
  8. for (j = 6; j < c->argc; j++) {
  9. int moreargs = (c->argc-1) - j;
  10. // 如果是copy
  11. if (!strcasecmp(c->argv[j]->ptr,"copy")) {
  12. copy = 1;
  13. } else if (!strcasecmp(c->argv[j]->ptr,"replace")) { // 如果是replace
  14. replace = 1;
  15. } else if (!strcasecmp(c->argv[j]->ptr,"auth")) { // 如果需要验证密码
  16. if (!moreargs) {
  17. addReplyErrorObject(c,shared.syntaxerr);
  18. return;
  19. }
  20. j++;
  21. // 获取密码
  22. password = c->argv[j]->ptr;
  23. redactClientCommandArgument(c,j);
  24. } else if (!strcasecmp(c->argv[j]->ptr,"auth2")) {
  25. // ...
  26. } else if (!strcasecmp(c->argv[j]->ptr,"keys")) { // 如果一次迁移多个key
  27. if (sdslen(c->argv[3]->ptr) != 0) {
  28. addReplyError(c,
  29. "When using MIGRATE KEYS option, the key argument"
  30. " must be set to the empty string");
  31. return;
  32. }
  33. // 或取第一个key
  34. first_key = j+1;
  35. // 计算key的数量
  36. num_keys = c->argc - j - 1;
  37. break; /* All the remaining args are keys. */
  38. } else {
  39. addReplyErrorObject(c,shared.syntaxerr);
  40. return;
  41. }
  42. }
  43. /* 校验timeout和dbid的值 */
  44. if (getLongFromObjectOrReply(c,c->argv[5],&timeout,NULL) != C_OK ||
  45. getLongFromObjectOrReply(c,c->argv[4],&dbid,NULL) != C_OK)
  46. {
  47. return;
  48. }
  49. // 如果超时时间小于0,默认设置1000毫秒
  50. if (timeout <= 0) timeout = 1000;
  51. // 分配空间,kv记录在源节点中实际查找到的key
  52. ov = zrealloc(ov,sizeof(robj*)*num_keys);
  53. kv = zrealloc(kv,sizeof(robj*)*num_keys);
  54. int oi = 0;
  55. // 处理KEY
  56. for (j = 0; j < num_keys; j++) {
  57. // 如果可以从源节点查找到key
  58. if ((ov[oi] = lookupKeyRead(c->db,c->argv[first_key+j])) != NULL) {
  59. // 记录查找到的key
  60. kv[oi] = c->argv[first_key+j];
  61. // 记录查找到的个数
  62. oi++;
  63. }
  64. }
  65. // 只处理实际查找到的key
  66. num_keys = oi;
  67. // 如果为0,不进行处理
  68. if (num_keys == 0) {
  69. zfree(ov); zfree(kv); // 释放空间
  70. addReplySds(c,sdsnew("+NOKEY\r\n")); // 返回NOKEY响应
  71. return;
  72. }
  73. try_again:
  74. write_error = 0;
  75. /* 与目标节点建立连接 */
  76. cs = migrateGetSocket(c,c->argv[1],c->argv[2],timeout);
  77. if (cs == NULL) {
  78. zfree(ov); zfree(kv);
  79. return; /* error sent to the client by migrateGetSocket() */
  80. }
  81. // 初始化缓冲区
  82. rioInitWithBuffer(&cmd,sdsempty());
  83. /* 如果密码不为空,验证密码 */
  84. if (password) {
  85. int arity = username ? 3 : 2;
  86. serverAssertWithInfo(c,NULL,rioWriteBulkCount(&cmd,'*',arity));
  87. serverAssertWithInfo(c,NULL,rioWriteBulkString(&cmd,"AUTH",4));
  88. if (username) {
  89. serverAssertWithInfo(c,NULL,rioWriteBulkString(&cmd,username,
  90. sdslen(username)));
  91. }
  92. serverAssertWithInfo(c,NULL,rioWriteBulkString(&cmd,password,
  93. sdslen(password)));
  94. }
  95. // ...
  96. // 处理KEY,只保留未过期的KEY
  97. for (j = 0; j < num_keys; j++) {
  98. long long ttl = 0;
  99. // 获取KEY的过期时间,返回-1表示未设置过期时间,否则返回过期时间
  100. long long expireat = getExpire(c->db,kv[j]);
  101. // 如果设置了过期时间
  102. if (expireat != -1) {
  103. // 计算ttl:过期时间减去当前时间
  104. ttl = expireat-mstime();
  105. // 如果已过期
  106. if (ttl < 0) {
  107. continue;
  108. }
  109. if (ttl < 1) ttl = 1;
  110. }
  111. /* 记录未过期的KEY */
  112. ov[non_expired] = ov[j];
  113. kv[non_expired++] = kv[j];
  114. serverAssertWithInfo(c,NULL,
  115. rioWriteBulkCount(&cmd,'*',replace ? 5 : 4));
  116. // 是否启用集群
  117. if (server.cluster_enabled)
  118. serverAssertWithInfo(c,NULL,
  119. rioWriteBulkString(&cmd,"RESTORE-ASKING",14)); // 将RESTORE-ASKING命令写入缓冲区
  120. else
  121. serverAssertWithInfo(c,NULL,rioWriteBulkString(&cmd,"RESTORE",7)); // 如果未开启集群将RESTORE命令写入缓冲区
  122. serverAssertWithInfo(c,NULL,sdsEncodedObject(kv[j]));
  123. // 将key写入缓冲区
  124. serverAssertWithInfo(c,NULL,rioWriteBulkString(&cmd,kv[j]->ptr,
  125. sdslen(kv[j]->ptr)));
  126. // 将ttl写入缓存区
  127. serverAssertWithInfo(c,NULL,rioWriteBulkLongLong(&cmd,ttl));
  128. /* 创建payload,将RDB版本、CRC64校验和以及value内容写入 */
  129. createDumpPayload(&payload,ov[j],kv[j]);
  130. // 将payload数据写入缓冲区
  131. serverAssertWithInfo(c,NULL,
  132. rioWriteBulkString(&cmd,payload.io.buffer.ptr,
  133. sdslen(payload.io.buffer.ptr)));
  134. sdsfree(payload.io.buffer.ptr);
  135. /* 如果设置了REPLACE参数,将REPLACE写入缓冲区 */
  136. if (replace)
  137. serverAssertWithInfo(c,NULL,rioWriteBulkString(&cmd,"REPLACE",7));
  138. }
  139. /* 更新实际需要处理的key */
  140. num_keys = non_expired;
  141. /* 将缓冲区的数据按照64K的块大小发送到目标节点 */
  142. errno = 0;
  143. {
  144. sds buf = cmd.io.buffer.ptr;
  145. size_t pos = 0, towrite;
  146. int nwritten = 0;
  147. while ((towrite = sdslen(buf)-pos) > 0) {
  148. // 需要发送的数据,如果超过了64K就按照64K的大小发送
  149. towrite = (towrite > (64*1024) ? (64*1024) : towrite);
  150. // 发送数据
  151. nwritten = connSyncWrite(cs->conn,buf+pos,towrite,timeout);
  152. if (nwritten != (signed)towrite) {
  153. write_error = 1;
  154. goto socket_err;
  155. }
  156. pos += nwritten;
  157. }
  158. }
  159. // 省略...
  160. }

createDumpPayload

createDumpPayload函数在cluster.c文件中:

  1. /* -----------------------------------------------------------------------------
  2. * DUMP, RESTORE and MIGRATE commands
  3. * -------------------------------------------------------------------------- */
  4. void createDumpPayload(rio *payload, robj *o, robj *key) {
  5. unsigned char buf[2];
  6. uint64_t crc;
  7. // 初始化缓冲区
  8. rioInitWithBuffer(payload,sdsempty());
  9. // 将value的数据类型写入缓冲区
  10. serverAssert(rdbSaveObjectType(payload,o));
  11. // 将value写入缓冲区
  12. serverAssert(rdbSaveObject(payload,o,key));
  13. /* Write the footer, this is how it looks like:
  14. * ----------------+---------------------+---------------+
  15. * ... RDB payload | 2 bytes RDB version | 8 bytes CRC64 |
  16. * ----------------+---------------------+---------------+
  17. * RDB version and CRC are both in little endian.
  18. */
  19. /* 设置RDB版本 */
  20. buf[0] = RDB_VERSION & 0xff;
  21. buf[1] = (RDB_VERSION >> 8) & 0xff;
  22. payload->io.buffer.ptr = sdscatlen(payload->io.buffer.ptr,buf,2);
  23. /* 设置CRC64校验和用于校验数据 */
  24. crc = crc64(0,(unsigned char*)payload->io.buffer.ptr,
  25. sdslen(payload->io.buffer.ptr));
  26. memrev64ifbe(&crc);
  27. payload->io.buffer.ptr = sdscatlen(payload->io.buffer.ptr,&crc,8);
  28. }

目标节点处理数据

restoreCommand

目标节点收到迁移的数据的处理逻辑在restoreCommand中(cluster.c文件中):

  1. 解析请求中的参数,判断是否有replace
  2. 如果没有replace并且key已经在当前节点存在,返回错误信息
  3. 调用verifyDumpPayload函数校验RDB版本和CRC校验和
  4. 从请求中解析value的数据类型和value值
  5. 如果设置了replace先删除数据库中存在的key
  6. 将key和vlaue添加到节点的数据库中
  1. /* RESTORE key ttl serialized-value [REPLACE] */
  2. void restoreCommand(client *c) {
  3. long long ttl, lfu_freq = -1, lru_idle = -1, lru_clock = -1;
  4. rio payload;
  5. int j, type, replace = 0, absttl = 0;
  6. robj *obj;
  7. /* 解析请求中的参数 */
  8. for (j = 4; j < c->argc; j++) {
  9. int additional = c->argc-j-1;
  10. if (!strcasecmp(c->argv[j]->ptr,"replace")) { // 如果有replace
  11. replace = 1; // 标记
  12. }
  13. // ...
  14. else {
  15. addReplyErrorObject(c,shared.syntaxerr);
  16. return;
  17. }
  18. }
  19. /* 如果没有replace并且key已经在数据库存在,返回错误信息 */
  20. robj *key = c->argv[1];
  21. if (!replace && lookupKeyWrite(c->db,key) != NULL) {
  22. addReplyErrorObject(c,shared.busykeyerr);
  23. return;
  24. }
  25. /* Check if the TTL value makes sense */
  26. if (getLongLongFromObjectOrReply(c,c->argv[2],&ttl,NULL) != C_OK) {
  27. return;
  28. } else if (ttl < 0) {
  29. addReplyError(c,"Invalid TTL value, must be >= 0");
  30. return;
  31. }
  32. /* 校验RDB版本和CRC */
  33. if (verifyDumpPayload(c->argv[3]->ptr,sdslen(c->argv[3]->ptr)) == C_ERR)
  34. {
  35. addReplyError(c,"DUMP payload version or checksum are wrong");
  36. return;
  37. }
  38. rioInitWithBuffer(&payload,c->argv[3]->ptr);
  39. // 解析value的数据类型和value值
  40. if (((type = rdbLoadObjectType(&payload)) == -1) ||
  41. ((obj = rdbLoadObject(type,&payload,key->ptr)) == NULL))
  42. {
  43. addReplyError(c,"Bad data format");
  44. return;
  45. }
  46. int deleted = 0;
  47. // 如果设置了replace
  48. if (replace)
  49. deleted = dbDelete(c->db,key); // 先删除数据库中存在的key
  50. if (ttl && !absttl) ttl+=mstime();
  51. if (ttl && checkAlreadyExpired(ttl)) {
  52. if (deleted) {
  53. rewriteClientCommandVector(c,2,shared.del,key);
  54. signalModifiedKey(c,c->db,key);
  55. notifyKeyspaceEvent(NOTIFY_GENERIC,"del",key,c->db->id);
  56. server.dirty++;
  57. }
  58. decrRefCount(obj);
  59. addReply(c, shared.ok);
  60. return;
  61. }
  62. /* 将key和vlaue添加到节点的数据库中 */
  63. dbAdd(c->db,key,obj);
  64. if (ttl) {
  65. setExpire(c,c->db,key,ttl);
  66. }
  67. objectSetLRUOrLFU(obj,lfu_freq,lru_idle,lru_clock,1000);
  68. signalModifiedKey(c,c->db,key);
  69. notifyKeyspaceEvent(NOTIFY_GENERIC,"restore",key,c->db->id);
  70. addReply(c,shared.ok);
  71. server.dirty++;
  72. }

标记迁移结果

数据迁移的最后一步,需要使用CLUSTER SETSLOT命令,在源节点和目标节点执行以下命令,标记slot最终所属的节点,并清除第一步中标记的迁移信息

:哈希槽

:哈希槽最终所在节点id

  1. CLUSTER SETSLOT <slot> NODE <node>

clusterCommand

CLUSTER SETSLOT <slot> NODE <node>命令的处理依旧在clusterCommand函数中,处理逻辑如下:

  1. 根据命令中传入的nodeid查找节点记为n,如果未查询到,返回错误信息
  2. 果slot已经在当前节点,但是根据nodeid查找到的节点n不是当前节点,说明slot所属节点与命令中指定的节点不一致,返回错误信息
  3. 在源节点上执行命令时,如果slot中key的数量为0,表示slot上的数据都已迁移完毕,而migrating_slots_to[slot]记录了slot迁移到的目标节点,既然数据已经迁移完成此时需要将migrating_slots_to[slot]迁出信息清除
  4. 调用clusterDelSlot函数先将slot删除
    • 获取slot所属节点
    • 将slot所属节点ClusterNode结构体中的slots数组对应的标记位取消,表示节点不再负责此slot
    • 将slot所属节点ClusterState结构体中的slots数组对应元素置为NULL,表示当前slot所属节点为空
  5. 调用clusterAddSlot将slot添加到最终所属的节点中
  6. 在目标节点上执行命令时,如果slot所属节点为当前节点,并且importing_slots_from[slot]不为空, importing_slots_from[slot]中记录了slot是从哪个节点迁移过来,此时数据已经迁移完毕,清除 importing_slots_from[slot]中的迁入信息
  1. void clusterCommand(client *c) {
  2. if (server.cluster_enabled == 0) {
  3. addReplyError(c,"This instance has cluster support disabled");
  4. return;
  5. }
  6. if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
  7. // ...
  8. }
  9. // ...
  10. else if (!strcasecmp(c->argv[1]->ptr,"setslot") && c->argc >= 4) { // 处理setslot命令
  11. int slot;
  12. clusterNode *n;
  13. if (nodeIsSlave(myself)) {
  14. addReplyError(c,"Please use SETSLOT only with masters.");
  15. return;
  16. }
  17. if ((slot = getSlotOrReply(c,c->argv[2])) == -1) return;
  18. if (!strcasecmp(c->argv[3]->ptr,"migrating") && c->argc == 5) {
  19. // migrating处理
  20. // ...
  21. } else if (!strcasecmp(c->argv[3]->ptr,"importing") && c->argc == 5) {
  22. // importing处理
  23. // ...
  24. } else if (!strcasecmp(c->argv[3]->ptr,"stable") && c->argc == 4) {
  25. // stable处理
  26. // ...
  27. } else if (!strcasecmp(c->argv[3]->ptr,"node") && c->argc == 5) {
  28. /* CLUSTER SETSLOT <SLOT> NODE <NODE ID> 命令处理 */
  29. // 根据nodeid查找节点
  30. clusterNode *n = clusterLookupNode(c->argv[4]->ptr);
  31. // 如果未查询到,返回错误信息
  32. if (!n) {
  33. addReplyErrorFormat(c,"Unknown node %s",
  34. (char*)c->argv[4]->ptr);
  35. return;
  36. }
  37. /* 如果slot已经在当前节点,但是根据node id查找到的节点不是当前节点,返回错误信息*/
  38. if (server.cluster->slots[slot] == myself && n != myself) {
  39. if (countKeysInSlot(slot) != 0) {
  40. addReplyErrorFormat(c,
  41. "Can't assign hashslot %d to a different node "
  42. "while I still hold keys for this hash slot.", slot);
  43. return;
  44. }
  45. }
  46. /* 在源节点上执行命令时 */
  47. /* 如果slot中key的数量为0,表示slot上的数据都已迁移完毕,而migrating_slots_to[slot]记录了slot迁移到的目标节点,既然数据已经迁移完成此时可以将迁移信息清除*/
  48. if (countKeysInSlot(slot) == 0 &&
  49. server.cluster->migrating_slots_to[slot])
  50. server.cluster->migrating_slots_to[slot] = NULL;// 清除迁移信息
  51. // 先删除slot
  52. clusterDelSlot(slot);
  53. // 添加slot到节点n
  54. clusterAddSlot(n,slot);
  55. /* 在目标节点上执行命令时 */
  56. /* 如果slot所属节点为当前节点,并且importing_slots_from[slot]不为空, importing_slots_from[slot]中记录了slot是从哪个节点迁移过来*/
  57. if (n == myself &&
  58. server.cluster->importing_slots_from[slot])
  59. {
  60. /* 更新节点的configEpoch */
  61. if (clusterBumpConfigEpochWithoutConsensus() == C_OK) {
  62. serverLog(LL_WARNING,
  63. "configEpoch updated after importing slot %d", slot);
  64. }
  65. // 清除importing_slots_from[slot]迁移信息
  66. server.cluster->importing_slots_from[slot] = NULL;
  67. /* 广播PONG消息,让其他节点尽快知道slot的最新信息 */
  68. clusterBroadcastPong(CLUSTER_BROADCAST_ALL);
  69. }
  70. } else {
  71. addReplyError(c,
  72. "Invalid CLUSTER SETSLOT action or number of arguments. Try CLUSTER HELP");
  73. return;
  74. }
  75. clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|CLUSTER_TODO_UPDATE_STATE);
  76. addReply(c,shared.ok);
  77. }
  78. // ...
  79. else {
  80. addReplySubcommandSyntaxError(c);
  81. return;
  82. }
  83. }

总结

参考

极客时间 - Redis源码剖析与实战(蒋德钧)

Redis版本:redis-6.2.5

【Redis】集群数据迁移的更多相关文章

  1. redis集群数据迁移

    redis集群数据备份迁移方案 n  迁移环境描述及分析 当前我们面临的数据迁移环境是:集群->集群. 源集群: 源集群为6节点,3主3备 主 备 192.168.112.33:8001 192 ...

  2. redis集群数据迁移txt版

    ./redis-trib.rb create --replicas 1 192.168.112.33:8001 192.168.112.33:8002 192.168.112.33:8003 192. ...

  3. 从零自学Hadoop(17):Hive数据导入导出,集群数据迁移下

    阅读目录 序 将查询的结果写入文件系统 集群数据迁移一 集群数据迁移二 系列索引 本文版权归mephisto和博客园共有,欢迎转载,但须保留此段声明,并给出原文链接,谢谢合作. 文章是哥(mephis ...

  4. elasticsearch7.5.0+kibana-7.5.0+cerebro-0.8.5集群生产环境安装配置及通过elasticsearch-migration工具做新老集群数据迁移

    一.服务器准备 目前有两台128G内存服务器,故准备每台启动两个es实例,再加一台虚机,共五个节点,保证down一台服务器两个节点数据不受影响. 二.系统初始化 参见我上一篇kafka系统初始化:ht ...

  5. redis集群在线迁移第一篇(数据在线迁移至新集群)实战一

    迁移背景:1.原来redis集群在A机房,需要把其迁移到新机房B上来.2.保证现有环境稳定.3.采用在线迁移方式,因为原有redis集群内有大量数据.4.如果是一个全新的redis集群搭建会简单很多. ...

  6. redis集群在线迁移第二篇(redis迁移后调整主从关系,停掉14机器上的所有从节点)-实战二

    变更需求为: 1.调整主从关系,所有节点都调整到10.129.51.30机器上 2.停掉10.128.51.14上的所有redis,14机器关机 14机器下线迁移至新机房,这段时间将不能提供服务. 当 ...

  7. 从零自学Hadoop(16):Hive数据导入导出,集群数据迁移上

    阅读目录 序 导入文件到Hive 将其他表的查询结果导入表 动态分区插入 将SQL语句的值插入到表中 模拟数据文件下载 系列索引 本文版权归mephisto和博客园共有,欢迎转载,但须保留此段声明,并 ...

  8. elasticsearch跨集群数据迁移

    写这篇文章,主要是目前公司要把ES从2.4.1升级到最新版本7.8,不过现在是7.9了,官方的文档:https://www.elastic.co/guide/en/elasticsearch/refe ...

  9. Redis集群数据没法拆分时的搭建策略

    在上一篇文章中,针对服务器单点.单例.单机存在的问题: 单点故障 容量有限 可支持的连接有限(性能不足) 提出了解决的办法:根据AKF原则搭建集群,大意是先X轴拆分,创建单机的镜像,组成主主.主备.主 ...

随机推荐

  1. Blazor组件提交全记录: FullScreen 全屏按钮/全屏服务 (BootstrapBlazor - Bootstrap 风格的 Blazor UI 组件库)

    Blazor 简介 Blazor 是一个使用 .NET 生成的交互式客户端 Web UI 的框架.和前端同学所熟知的 Vue.React.Angular 有巨大差异. 其最大的特色是使用 C# 代码( ...

  2. drf的JWT认证

    JWT认证(5星) token发展史 在用户注册或登录后,我们想记录用户的登录状态,或者为用户创建身份认证的凭证.我们不再使用Session认证机制,而使用Json Web Token(本质就是tok ...

  3. 进程的概念及multiprocess模块的使用

    一.进程 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础.在早期面向进程设计的计算机结构中,进程是程序的基本执行实体:在 ...

  4. .NET 6 史上最全攻略

    欢迎使用.NET 6.今天的版本是.NET 团队和社区一年多努力的结果.C# 10 和F# 6 提供了语言改进,使您的代码更简单.更好.性能大幅提升,我们已经看到微软降低了托管云服务的成本..NET ...

  5. 如何调试手机上的网页以及基于Cordova/Phonegap的Hybrid应用

    开发手机页面以及Hybird应用时,调试曾经是个老大难问题,不时需要用写log等方式曲线救国. 实际上,Chrome和Android(需要4.4+版本)已经提供了不亚于电脑版本的调试功能,只是看样子还 ...

  6. 介绍关于MSSQL当前行中获取到上一行某列值的函数 Coalesce

    记录一个小知识点,在SQLGrid中,在当前行显示上一行某列值的函数** Coalesce **的使用. 显示上一行是有啥子用? 经常有人百度SQL上一行减下一行的写法,但是没几个文章是用最简单直接的 ...

  7. vue项目中的去抖与节流

    节流 // fn是我们需要包装的事件回调, interval是时间间隔的阈值 function throttle(fn, interval) { let last = 0; // last为上一次触发 ...

  8. BUUCTF-Web:[GXYCTF2019]Ping Ping Ping

    题目 解题过程 1.题目页面提示?ip=,猜测是让我们把这个当做变量上传参数,由此猜想是命令注入 2.用管道符加上linux常用命令ls(windwos可以尝试dir)试试 所谓管道符(linux)的 ...

  9. Vue 学习之路(一)- 创建脚手架并创建项目

    安装脚手架 命令 npm install -g @vue/cli 打开 cmd 窗口输入以上命令.当出现以下界面即表示安装完成. 查看已安装脚手架版本 命令 vue -V 在 cmd 窗口输入以上命令 ...

  10. 如何突破Jenkins瓶颈,实现集中管理、灵活高效的CI/CD

    在过去的几年间,随着DevOps的兴起,持续集成(Continuous Integration)与持续交付(Continuous Delivery)的热度也水涨船高.在本文中,我们将首先带您了解热门的 ...