Fork me on GitHub

版权声明 本站原创文章 由 萌叔 发表
转载请注明 萌叔 | https://vearne.cc

1. 引言

我们都知道在Redis-Cluster集群模式下,集群中有18634个slot,slot分布在集群多个实例上,当执行一个Command时,Redis客户端会提取Command中的key
根据下面的算法得出key所属的slot

slot=CRC16(key)&16383

在根据客户端中的路由表,找到slot所在的Redis实例
这里的路由表存储的就是

slot -> redis-instance

那么问题来了redis客户端是如何得到这个路由表的呢?

2. 分析

下面以go-redis/redis的代码为例,谈谈Redis客户端如何获取和维护slot路由信息。

2.1 存储结构

type clusterClient struct {
    opt           *ClusterOptions
    nodes         *clusterNodes
    state         *clusterStateHolder // 在这里
    cmdsInfoCache *cmdsInfoCache      //nolint:structcheck
}
type clusterState struct {
    nodes   *clusterNodes
    Masters []*clusterNode
    Slaves  []*clusterNode

    slots []*clusterSlot  // 路由信息存储在这里

    generation uint32
    createdAt  time.Time
}
type clusterSlot struct {
    start, end int
    nodes      []*clusterNode
}

2.2 命令执行过程

  • 1)通过key计算出对应slot
  • 2)通过路由表查找到对应的node信息
  • 3)向node发送CMD

其实第2步根据slot从clusterState中查询对应clusterNode

func (c *clusterState) slotNodes(slot int) []*clusterNode {
    i := sort.Search(len(c.slots), func(i int) bool {
        return c.slots[i].end >= slot
    })
    if i >= len(c.slots) {
        return nil
    }
    x := c.slots[i]
    if slot >= x.start && slot <= x.end {
        return x.nodes
    }
    return nil
}

2.3 初始化slot路由信息

在处理任意Command时,如果路由信息为空,会触发以下动作

ClusterClient.process() -> ClusterClient.cmdNode() -> clusterStateHolder.Get() -> clusterStateHolder.Reload() -> ClusterClient.loadState()

最终调用了

func (c cmdable) ClusterSlots(ctx context.Context) *ClusterSlotsCmd {
    cmd := NewClusterSlotsCmd(ctx, "cluster", "slots")
    _ = c(ctx, cmd)
    return cmd
}

CLUSTER SLOTS的返回大体如下:

192.168.132.114:6403> cluster slots
1) 1) (integer) 0        // Start slot range
   2) (integer) 2730     // End slot range
   3) 1) "192.168.134.95"  // Redis实例-IP(主)
      2) (integer) 6403   // Redis实例-端口
      3) "702608b2556413c6f8927ef06e86e5c0e869a07d" // node ID
   4) 1) "192.168.134.109"  // Redis实例-IP(从)
      2) (integer) 6403     // Redis实例-端口
      3) "d950357fd1e0010646b32d3397ced3e52b1322fd"
2) 1) (integer) 8192
   2) (integer) 10922
   3) 1) "192.168.134.95"
      2) (integer) 6403
      3) "702608b2556413c6f8927ef06e86e5c0e869a07d"
   4) 1) "192.168.134.109"
      2) (integer) 6403
      3) "d950357fd1e0010646b32d3397ced3e52b1322fd"
3) 1) (integer) 2731
   2) (integer) 8191
   3) 1) "192.168.132.114"
      2) (integer) 6403
      3) "fccdf478a66f2a11713111b5567e3afa2be6c3ab"
   4) 1) "192.168.132.123"
      2) (integer) 6403
      3) "83600e144b488ad324235d84dca9ae6f02928715"
4) 1) (integer) 10923
   2) (integer) 16383
   3) 1) "192.168.132.124"
      2) (integer) 6403
      3) "f4347dfea9bf425281df393976b02547fa45c7ce"
   4) 1) "192.168.132.128"
      2) (integer) 6403
      3) "aba3f6cda55c0595a82d0ddea864a58c21008fb3"
start end node
0 2730 192.168.134.95/192.168.134.109
2731 8191 192.168.132.114/192.168.132.123
8192 10922 192.168.134.95/192.168.134.109
10923 16383 192.168.132.124/192.168.132.128

问题来了,在成功获取slot的路由信息后,集群中slot的分布情况是会发生变化的。比如Redis集群的管理员通过指令移动一部分slot。客户端怎么感知到这种变化?

(redis1)$ redis-cli --cluster reshard 192.168.11.131:6379 \
--cluster-from node1,nod2,node3 \
--cluster-to node4 \
--cluster-slots 3276 \
--cluster-yes

2.4 更新slot路由信息

1) 当slot路由信息已经超过10秒没有更新了,将触发LazyReload()

func (c *clusterStateHolder) Get(ctx context.Context) (*clusterState, error) {
    ...
    if time.Since(state.createdAt) > 10*time.Second {
        c.LazyReload()
    }
    ...
}

2) 执行Command的过程中,如果收到MOVED或ASK重定向,将触发LazyReload()

从moved的ERROR中可以获取slot所在的新的地址, 同时触发LazyReload()

func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
  ...
    moved, ask, addr = isMovedError(lastErr)
    if moved || ask {
        c.state.LazyReload()

        var err error
        node, err = c.nodes.GetOrCreate(addr)
        if err != nil {
            return err
        }
        continue
    }
  ...

}
192.168.132.114:6403> get a
(error) MOVED 15495 192.168.132.124:6403

MOVED重定向过程

3. 参考资料

1.CLUSTER SLOTS命令
2.Redis 5.0 redis-cli –cluster help说明
3.Redis Cluster 重定向问题 – Moved/Ask重定向


微信公众号

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据