美文网首页redis系列
redis系列(八):槽

redis系列(八):槽

作者: 范柏柏 | 来源:发表于2020-06-01 20:13 被阅读0次

    槽是什么

    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
    槽数据结构.png

    只有在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上。那么会发生如下动作。

    1. 在7000服务器上,客服端用户执行:
    set phone 123123123
    
    1. 7000计算phone的槽为10086,并判断归7001负责,返回给客户端:
    MOVED 10086 127.0.0.1:7001
    
    1. 客户端收到后,会自动把服务区转换到7001上,客户端自动继续执行,2-3步对用户无感知:
    set phone 123123123
    

    成功。

    为什么不用一致性哈希呢

    原理不太解释了,就一句话吧,一致性哈希对于数据分布、节点位置的控制不是很友好,这种轻便的哈希槽更符合redis这种场景。

    重新分片

    redis集群的重新分片操作是由redis的集群管理软件redis-trib执行负责的。步骤如下:

    1. redis-trib对目标节点发送cluster setslot <slot> migrating <target_id> 命令。让目标节点准备好从源节点导入(import)属于槽slot的键值对

    2. redis-trib对源节点发送 cluster setslot <slot> migrating <target_id> 命令,让源节点准备好将属于槽slot的键值对迁移(migrate)至目标节点

    3. redis-trib向源节点发送cluster getkeysinslot <slot> <count>命令,获取最多count个属于槽slot的键值对的键名(key name)

    4. 对步骤3获得的每个键名,redis-trib都向源节点发送一个migrate <target_ip> <target_port> <key_name> 0 <timeout>命令,将被选中的键原子地从源节点迁移至目标节点

    5. 重复执行3-4,直到源节点保存的所有属于槽slot的键值对都被迁移到目标节点为止。

    6. redis-trib向集群中的任意一个节点发送 cluster setslot <slot> node <target_id>命令,将槽slot指派给目标节点,这一消息会发送至整个集群,让集群所有节点都直到该slot被迁移到目标节点了。

    迁移期间访问问题

    当在迁移过程中,如果被访问的slot,可能会有部分key存在在源节点,有部分在目标节点中。

    1. 当客户端发送请求到源节点的时候,源节点会查看对应的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节点,存多少槽,存那些槽。是手动设置的。

    相关文章

      网友评论

        本文标题:redis系列(八):槽

        本文链接:https://www.haomeiwen.com/subject/oadhzhtx.html