槽是什么
redis集群的整个数据库被分为16384(2的14次方)个槽(slot),数据库中的每个key都属于这16384个槽的其中一个,一个槽可以管理多个key,一个key值属于一个槽。
集群中每个节点都可以处理0-16384个槽,如果集群中只有一个节点,那么这个节点就是自己处理所有的槽。
当16384个槽都有节点在处理时,集群处于上线状态(ok),相反,有任意一个槽没有得到处理,就处于下线状态(fail)。
在7000节点上执行:
cluster addslots <slot> [slot ...]
cluster addslots 0 1 2 3 4 ... 5000
那么槽0-槽5000指派给节点7000负责。
再手动把5001-10000指派给7001,10001-16383指派给7002。现在槽都指派了,集群处于上线状态。
记录槽信息
很显然,记录一个节点负责哪些槽,肯定是节点自己维护的。那肯定就在clusterNode
里了。
struct clusterNode {
// ... 上一章节记录的属性
// 二进制位数组,数组长度为16384/8=2048字节,刚好包括16384个二进制位。
unsigned char slots[16384/8];
// 记录该节点处理槽的数量,也就是slots数组中值为1的二进制位的数量
int numslots;
// ...
}
redis以0为起始索引,16383为终止索引,对slots数组中的16384个二进制位进行编号,根据索引i上的二进制位,判断节点是否负责处理槽i。
- 在索引i上的值为1 --> 表示节点负责处理槽i
- 在索引i上的值为0 --> 表示节点不负责处理槽i
只有在slots[0]中的0-7位是1,表示这个节点值负责槽0槽7。
看一下时间复杂度。
取出和设置slots数组中的任意一个二进制位的复杂度都是O(1),所以
- 检查该节点是否负责处理某个槽
- 将某个槽支配给该节点
这两个操作的时间复杂度都是O(1)
那,查询某一个槽是哪个节点负责的呢。以当前的结构需要遍历一遍集群中的redis,复杂度为O(n),所以还会有一个数据结构。
很显然,记录槽在哪个节点上,肯定是集群维护的,那肯定就在clusterState里了
typedef struct clusterState {
// ... 上一章节记录的属性
// key 为槽索引,槽1就是slots[1]
// value 为负责该槽的节点 clusterNode
// 如果没有节点负责,那么指向 null
clusterNode *slots[16384];
// ...
}
有这个属性了,查询某一个槽是哪个节点负责的,时间复杂度也就O(1)了
现在合起来就有一张集群的大图了
槽完整大图.png计算key属于哪个槽
def slot_number(key):
return CRC16(key) % 16383
先计算key的CRC-16校验和, % 16383位运算计算出16383之间的整数,以这个数作为key的槽号。
使用
cluster keyslot <key> 命令 可以查看一个给定键属于哪个槽
当计算出key所在的槽号后,节点就会检查这个槽号归不归自己管,在clusterState.slots中O(1)判断。
- clusterState.slots[i] 等于 clusterState.shelf,那么归自己管,直接执行客户端发送的命令
- 如果不等于。该节点会返回clusterState.slots[i]指向的节点信息,向客户端返回MOVED错误,指引客户端转向至正确的节点。
举个例子:当在7000这台机器执行命令的时候,7000发现这个key在7001上。那么会发生如下动作。
- 在7000服务器上,客服端用户执行:
set phone 123123123
- 7000计算phone的槽为10086,并判断归7001负责,返回给客户端:
MOVED 10086 127.0.0.1:7001
- 客户端收到后,会自动把服务区转换到7001上,客户端自动继续执行,2-3步对用户无感知:
set phone 123123123
成功。
为什么不用一致性哈希呢
原理不太解释了,就一句话吧,一致性哈希对于数据分布、节点位置的控制不是很友好,这种轻便的哈希槽更符合redis这种场景。
重新分片
redis集群的重新分片操作是由redis的集群管理软件redis-trib执行负责的。步骤如下:
-
redis-trib对目标节点发送cluster setslot <slot> migrating <target_id> 命令。让目标节点准备好从源节点导入(import)属于槽slot的键值对
-
redis-trib对源节点发送 cluster setslot <slot> migrating <target_id> 命令,让源节点准备好将属于槽slot的键值对迁移(migrate)至目标节点
-
redis-trib向源节点发送cluster getkeysinslot <slot> <count>命令,获取最多count个属于槽slot的键值对的键名(key name)
-
对步骤3获得的每个键名,redis-trib都向源节点发送一个migrate <target_ip> <target_port> <key_name> 0 <timeout>命令,将被选中的键原子地从源节点迁移至目标节点
-
重复执行3-4,直到源节点保存的所有属于槽slot的键值对都被迁移到目标节点为止。
-
redis-trib向集群中的任意一个节点发送 cluster setslot <slot> node <target_id>命令,将槽slot指派给目标节点,这一消息会发送至整个集群,让集群所有节点都直到该slot被迁移到目标节点了。
迁移期间访问问题
当在迁移过程中,如果被访问的slot,可能会有部分key存在在源节点,有部分在目标节点中。
- 当客户端发送请求到源节点的时候,源节点会查看对应的key是否还在本节点,如果存在,则直接执行命令返回给客户。如果不存在,则会给客户端返回一个ASK错误,指引客户端往正在导入的目标slot去请求对应的key。客户端可以通过返回的ASK错误中的目标节点进行对应KEY的请求。
2) 当客户端发送请求到目标节点时。
- 如果客户端请求时,带上ASKING标识,由目标节点会执行对应KEY的查询。正常情况下,如果是通过查询源slot,获取ASK错误之后,再到目标节点进行查询的时候,需要带上ASKING标识。
- 如果客户端请求时,未带上ASKING标识,原由上,对应的slot还属于源节点,则目标节点会拒绝执行KEY查询,会返回一个MOVED错误给客户端,告诉客户端对应的KEY的slot属于源节点。正常情况下,如果第一次请求KEY到了正在迁移的目标节点,则会收到MOVED错误。
这里有个小知识点,ASK错误,和MOVED错误相似,是在重新分片的过程中发生key的转移导致的未命中重定向。ASK只是两个节点在槽迁移过程中的临时措施。
知识点:每个redis节点,存多少槽,存那些槽。是手动设置的。
网友评论