Redis 高可用的最后一公里:Cluster 分片、Gossip 与故障转移全流程

主从复制加哨兵机制解决了读写分离和故障转移,但解决不了单机容量瓶颈。本文从"为什么需要 Cluster"切入,按照"数据怎么分片 → 客户端怎么路由 → 怎么不停机扩容 → 节点怎么通信 → 怎么故障转移 → 多 key 怎么处理"的思路,系统讲清楚 Redis Cluster 的核心机制,最后回答为什么槽的数量是 16384。


一、主从+哨兵之后,还差什么

在上一篇文章中,我们讲清楚了 Redis 主从复制和哨兵机制:

  • 主从复制解决了读性能瓶颈和数据冗余问题。
  • Sentinel 哨兵机制解决了主节点故障后的自动切换问题。

如果不清楚主从+哨兵机制的可以去查看我的上一篇博客 : 一文讲透 Redis 高可用:主从复制、哨兵机制与故障转移全流程

看起来高可用的核心问题已经被解决了,但实际上还有一个被忽略的问题:容量

1.1 单机 Redis 的容量上限

即使部署了一个主节点加五个从节点,整个 Redis 能存储的数据量仍然只等于主节点上的数据量。从节点保存的只是副本,并不会让总数据量变大。

换句话说:

主从架构扩展的是读写能力,而不是存储容量。

而单机 Redis 的存储量是有上限的。Redis 的数据主要存在内存中,当数据量大到一定程度(比如超过 64GB)时,fork 和 RDB 的生成会变得非常慢,很可能卡死主线程,拖慢整个节点的响应速度。

这就引出了一个新的核心问题:

如何突破单机内存限制,让 Redis 能存储更多数据?

1.2 切片思想:把数据分散到多个节点

答案是数据分片(Sharding)------把数据切成几片,分散存储到多个节点上,每个节点只负责一部分数据。

Redis Cluster 就是这种切片思想的一种实现。它不是一个简单的主从集群,而是一个去中心化的分布式集群:多个主节点各自负责一部分数据,同时每个主节点可以有自己的从节点做备份。
#mermaid-svg-fjK9ZbUJQO34cHIN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fjK9ZbUJQO34cHIN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fjK9ZbUJQO34cHIN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fjK9ZbUJQO34cHIN .error-icon{fill:#552222;}#mermaid-svg-fjK9ZbUJQO34cHIN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fjK9ZbUJQO34cHIN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fjK9ZbUJQO34cHIN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fjK9ZbUJQO34cHIN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fjK9ZbUJQO34cHIN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fjK9ZbUJQO34cHIN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fjK9ZbUJQO34cHIN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fjK9ZbUJQO34cHIN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fjK9ZbUJQO34cHIN .marker.cross{stroke:#333333;}#mermaid-svg-fjK9ZbUJQO34cHIN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fjK9ZbUJQO34cHIN p{margin:0;}#mermaid-svg-fjK9ZbUJQO34cHIN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fjK9ZbUJQO34cHIN .cluster-label text{fill:#333;}#mermaid-svg-fjK9ZbUJQO34cHIN .cluster-label span{color:#333;}#mermaid-svg-fjK9ZbUJQO34cHIN .cluster-label span p{background-color:transparent;}#mermaid-svg-fjK9ZbUJQO34cHIN .label text,#mermaid-svg-fjK9ZbUJQO34cHIN span{fill:#333;color:#333;}#mermaid-svg-fjK9ZbUJQO34cHIN .node rect,#mermaid-svg-fjK9ZbUJQO34cHIN .node circle,#mermaid-svg-fjK9ZbUJQO34cHIN .node ellipse,#mermaid-svg-fjK9ZbUJQO34cHIN .node polygon,#mermaid-svg-fjK9ZbUJQO34cHIN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fjK9ZbUJQO34cHIN .rough-node .label text,#mermaid-svg-fjK9ZbUJQO34cHIN .node .label text,#mermaid-svg-fjK9ZbUJQO34cHIN .image-shape .label,#mermaid-svg-fjK9ZbUJQO34cHIN .icon-shape .label{text-anchor:middle;}#mermaid-svg-fjK9ZbUJQO34cHIN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fjK9ZbUJQO34cHIN .rough-node .label,#mermaid-svg-fjK9ZbUJQO34cHIN .node .label,#mermaid-svg-fjK9ZbUJQO34cHIN .image-shape .label,#mermaid-svg-fjK9ZbUJQO34cHIN .icon-shape .label{text-align:center;}#mermaid-svg-fjK9ZbUJQO34cHIN .node.clickable{cursor:pointer;}#mermaid-svg-fjK9ZbUJQO34cHIN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fjK9ZbUJQO34cHIN .arrowheadPath{fill:#333333;}#mermaid-svg-fjK9ZbUJQO34cHIN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fjK9ZbUJQO34cHIN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fjK9ZbUJQO34cHIN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fjK9ZbUJQO34cHIN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fjK9ZbUJQO34cHIN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fjK9ZbUJQO34cHIN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fjK9ZbUJQO34cHIN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fjK9ZbUJQO34cHIN .cluster text{fill:#333;}#mermaid-svg-fjK9ZbUJQO34cHIN .cluster span{color:#333;}#mermaid-svg-fjK9ZbUJQO34cHIN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fjK9ZbUJQO34cHIN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fjK9ZbUJQO34cHIN rect.text{fill:none;stroke-width:0;}#mermaid-svg-fjK9ZbUJQO34cHIN .icon-shape,#mermaid-svg-fjK9ZbUJQO34cHIN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fjK9ZbUJQO34cHIN .icon-shape p,#mermaid-svg-fjK9ZbUJQO34cHIN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fjK9ZbUJQO34cHIN .icon-shape .label rect,#mermaid-svg-fjK9ZbUJQO34cHIN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fjK9ZbUJQO34cHIN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fjK9ZbUJQO34cHIN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fjK9ZbUJQO34cHIN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Redis Cluster
分片 3
分片 2
分片 1
复制
复制
复制
客户端
Master A
Replica A'
Master B
Replica B'
Master C
Replica C'

1.3 Cluster 引入后的六大核心问题

引入数据分片后,新的问题随之而来:

  1. 数据怎么分片:数据按什么规则分配到不同节点?
  2. 怎么实现不停机扩容:集群节点变动时,如何让客户端无感知?
  3. 客户端怎么知道命令发给谁:客户端如何确定某条命令该发往哪个节点?
  4. 节点之间怎么通信:集群中的节点如何感知彼此的状态变化?
  5. 怎么实现故障转移:某个主节点挂了,如何自动切换?
  6. 多 key 命令怎么处理:一次操作多个 key,但这些 key 分布在不同节点怎么办?

本文接下来就围绕这六个问题展开。


二、数据怎么分片:哈希槽机制

2.1 朴素方案:直接对节点数取模

Redis 中的数据都是 KV 结构,所以数据分片本质上就是对 key 分片。

最简单的方法是:用哈希函数算出 key 的哈希值,再对节点数取模。

text 复制代码
slot = hash(key) % node_count

比如有 3 个节点,hash(key) = 10,则 10 % 3 = 1,数据存到节点 1。

这个方法简单直接,但有一个致命问题:集群节点数一旦变化,映射结果全部失效

比如集群从 3 个节点扩容到 5 个节点:

  • 之前 10 % 3 = 1,数据在节点 1。
  • 现在 10 % 5 = 0,数据应该在节点 0。

但数据实际上还在节点 1 上,客户端按新规则去找节点 0,就找不到了。

节点变动导致大量数据需要重新迁移,而且迁移期间客户端根本不知道数据在哪。

所以直接对节点数取模的方案不可行,需要引入一个中间层来解耦。

2.2 引入中间层:哈希槽

回顾前面的问题:直接对节点数取模之所以失败,是因为"Key 到节点"这条映射关系把两件事绑死在了一起------Key 的归属和节点的数量强耦合,节点数一变,所有 Key 的归属全变。

所以解决思路是解耦:把一条直接映射拆成两段,中间加一个中间层。
#mermaid-svg-jSVg5zUH9sT1Ld9k{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jSVg5zUH9sT1Ld9k .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jSVg5zUH9sT1Ld9k .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jSVg5zUH9sT1Ld9k .error-icon{fill:#552222;}#mermaid-svg-jSVg5zUH9sT1Ld9k .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jSVg5zUH9sT1Ld9k .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jSVg5zUH9sT1Ld9k .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jSVg5zUH9sT1Ld9k .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jSVg5zUH9sT1Ld9k .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jSVg5zUH9sT1Ld9k .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jSVg5zUH9sT1Ld9k .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jSVg5zUH9sT1Ld9k .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jSVg5zUH9sT1Ld9k .marker.cross{stroke:#333333;}#mermaid-svg-jSVg5zUH9sT1Ld9k svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jSVg5zUH9sT1Ld9k p{margin:0;}#mermaid-svg-jSVg5zUH9sT1Ld9k .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jSVg5zUH9sT1Ld9k .cluster-label text{fill:#333;}#mermaid-svg-jSVg5zUH9sT1Ld9k .cluster-label span{color:#333;}#mermaid-svg-jSVg5zUH9sT1Ld9k .cluster-label span p{background-color:transparent;}#mermaid-svg-jSVg5zUH9sT1Ld9k .label text,#mermaid-svg-jSVg5zUH9sT1Ld9k span{fill:#333;color:#333;}#mermaid-svg-jSVg5zUH9sT1Ld9k .node rect,#mermaid-svg-jSVg5zUH9sT1Ld9k .node circle,#mermaid-svg-jSVg5zUH9sT1Ld9k .node ellipse,#mermaid-svg-jSVg5zUH9sT1Ld9k .node polygon,#mermaid-svg-jSVg5zUH9sT1Ld9k .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jSVg5zUH9sT1Ld9k .rough-node .label text,#mermaid-svg-jSVg5zUH9sT1Ld9k .node .label text,#mermaid-svg-jSVg5zUH9sT1Ld9k .image-shape .label,#mermaid-svg-jSVg5zUH9sT1Ld9k .icon-shape .label{text-anchor:middle;}#mermaid-svg-jSVg5zUH9sT1Ld9k .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jSVg5zUH9sT1Ld9k .rough-node .label,#mermaid-svg-jSVg5zUH9sT1Ld9k .node .label,#mermaid-svg-jSVg5zUH9sT1Ld9k .image-shape .label,#mermaid-svg-jSVg5zUH9sT1Ld9k .icon-shape .label{text-align:center;}#mermaid-svg-jSVg5zUH9sT1Ld9k .node.clickable{cursor:pointer;}#mermaid-svg-jSVg5zUH9sT1Ld9k .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jSVg5zUH9sT1Ld9k .arrowheadPath{fill:#333333;}#mermaid-svg-jSVg5zUH9sT1Ld9k .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jSVg5zUH9sT1Ld9k .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jSVg5zUH9sT1Ld9k .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jSVg5zUH9sT1Ld9k .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jSVg5zUH9sT1Ld9k .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jSVg5zUH9sT1Ld9k .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jSVg5zUH9sT1Ld9k .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jSVg5zUH9sT1Ld9k .cluster text{fill:#333;}#mermaid-svg-jSVg5zUH9sT1Ld9k .cluster span{color:#333;}#mermaid-svg-jSVg5zUH9sT1Ld9k div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jSVg5zUH9sT1Ld9k .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jSVg5zUH9sT1Ld9k rect.text{fill:none;stroke-width:0;}#mermaid-svg-jSVg5zUH9sT1Ld9k .icon-shape,#mermaid-svg-jSVg5zUH9sT1Ld9k .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jSVg5zUH9sT1Ld9k .icon-shape p,#mermaid-svg-jSVg5zUH9sT1Ld9k .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jSVg5zUH9sT1Ld9k .icon-shape .label rect,#mermaid-svg-jSVg5zUH9sT1Ld9k .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jSVg5zUH9sT1Ld9k .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jSVg5zUH9sT1Ld9k .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jSVg5zUH9sT1Ld9k :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第一段:固定算法
第二段:可调整的映射表
Key
槽 0~16383
具体节点

这个中间层就是槽(Slot) 。Redis Cluster 定义了 16384 个槽,编号从 0 到 16383。

解耦之后,原来的一条映射变成了两段独立的映射:

  • 第一段:Key → 槽 。使用 CRC16(key) % 16384 计算出 key 属于哪个槽。这一段只依赖 key 本身和固定的槽总数,和集群有几个节点完全无关,所以这个映射关系是永久固定的。
  • 第二段:槽 → 节点。每个槽分配给某个主节点负责。这一段会随集群规模变化而调整,但变化频率很低。

解耦的核心价值在于:两段映射可以独立变化。扩容或缩容时,只需要调整第二段"槽 → 节点"的映射关系,把某些槽从旧节点改派给新节点,而第一段"Key → 槽"的计算方式完全不需要改动。

换句话说,Key 永远属于同一个槽,变的是这个槽归哪个节点管。数据迁移的粒度就变成了"搬整个槽",而不是"重新计算所有 key 的归属"。

2.3 槽的初始分配

集群初始化时,16384 个槽会被平均分配到各个主节点上。比如有 3 个主节点 A、B、C:

节点 负责槽范围 槽数量
节点 A 0 ~ 5460 5461
节点 B 5461 ~ 10922 5462
节点 C 10923 ~ 16383 5461

每个节点都会在本地维护一份完整的"哪个槽归谁管"的映射表,并且节点之间会不断同步这个信息,保证所有节点看到的映射关系是一致的。


三、客户端怎么知道给哪个节点发命令

3.1 从上一节的成果说起

上一节我们通过哈希槽解耦了 Key 和节点:Key 永远属于固定的槽,槽再映射到具体节点。这样一来,扩容时只需要调整"槽 → 节点"的映射,不会再出现"数据找不到"的问题。

但这只解决了数据该放在哪 的问题,还有一个问题没解决:客户端怎么知道该把命令发给谁

在哨兵模式中,这个问题是现成答案:客户端通过 Sentinel 集群来获取主节点地址,Sentinel 负责管理命令的发送目标。

但在 Redis Cluster 中并没有引入 Sentinel 机制(原因后面会详细讲),所以不能再依赖哨兵来路由命令。客户端必须自己想办法确定命令该发给哪个节点。

3.2 客户端本地缓存映射表

Redis Cluster 采取了一种更轻量的方案:

  1. 客户端启动时,连接集群中的任意一个节点,拉取一份完整的"槽 → 节点"映射表。
  2. 客户端在本地对 key 执行同样的 CRC16(key) % 16384 运算,算出 key 属于哪个槽。
  3. 根据本地映射表,找到负责该槽的节点。
  4. 直接把命令发给该节点。

集群节点 客户端 集群节点 客户端 #mermaid-svg-ZhZq2uFaPIU1VqLQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZhZq2uFaPIU1VqLQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .error-icon{fill:#552222;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .marker.cross{stroke:#333333;}#mermaid-svg-ZhZq2uFaPIU1VqLQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZhZq2uFaPIU1VqLQ p{margin:0;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ZhZq2uFaPIU1VqLQ text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ZhZq2uFaPIU1VqLQ .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-ZhZq2uFaPIU1VqLQ #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .sequenceNumber{fill:white;}#mermaid-svg-ZhZq2uFaPIU1VqLQ #sequencenumber{fill:#333;}#mermaid-svg-ZhZq2uFaPIU1VqLQ #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .messageText{fill:#333;stroke:none;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .labelText,#mermaid-svg-ZhZq2uFaPIU1VqLQ .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .loopText,#mermaid-svg-ZhZq2uFaPIU1VqLQ .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ZhZq2uFaPIU1VqLQ .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .noteText,#mermaid-svg-ZhZq2uFaPIU1VqLQ .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .actorPopupMenu{position:absolute;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-ZhZq2uFaPIU1VqLQ .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ZhZq2uFaPIU1VqLQ .actor-man circle,#mermaid-svg-ZhZq2uFaPIU1VqLQ line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-ZhZq2uFaPIU1VqLQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 拉取槽与节点的映射表 返回完整映射表 CRC16(key) % 16384 = 槽号 查本地映射表找到目标节点 直接发送命令到目标节点

这个方案高效且简单,但带来了一个新的问题:映射表会变

当集群扩容或缩容时,槽的归属会发生变化,客户端本地的映射表就可能过期。这就引出了下一节要讲的 MOVED 和 ASK 重定向机制。


四、怎么实现不停机扩容:MOVED 与 ASK 重定向

4.1 问题:迁移期间的命令路由

当新节点加入集群时,需要把一部分槽从旧节点迁移到新节点。这个迁移过程不是瞬间完成的,需要逐个 key 转移。

在迁移期间,会出现一个"窗口期"------部分数据已经搬走了,部分数据还在旧节点上。此时如果客户端的命令请求过来,它还是根据本地映射表把命令发给旧的节点。

旧节点收到命令后,有两种情况:

  • 数据还在旧节点:直接处理返回。
  • 数据已经迁移走:需要新的机制来处理。

4.2 槽位完整迁移完成后:MOVED 重定向

如果一个槽的数据已经全部迁移完成 ,即源节点上该槽的所有 key 都已经搬到目标节点,并且槽的归属已经正式切换,此时旧节点会返回一个 MOVED 命令:

text 复制代码
(error) MOVED 100 192.168.1.2:6379

含义是:槽 100 的数据已经迁移到 192.168.1.2:6379,请去新节点找。

客户端收到 MOVED 后会做两件事:

  1. 更新本地映射表,把槽 100 的归属改为新节点。
  2. 重新发送命令到新节点。

这里需要补充一个客户端实现的细节。在 Redis Cluster 协议层面,MOVED 只是告诉客户端"槽 100 归新节点了",客户端理论上只需要更新本地映射表中槽 100 这一条记录就够了。

但在实际的主流客户端(如 Jedis、Lettuce)中,收到 MOVED 后通常不会只更新单个槽,而是会主动向服务端发起一次 CLUSTER SLOTSCLUSTER NODES 命令,直接拉取一份完整的最新映射表来整体替换本地缓存。原因是:既然集群已经发生了槽位变动,大概率不止一个槽变了,与其逐个等 MOVED 被动更新,不如一次性主动拉全量,避免后续不断触发重定向。

所以完整的流程是:协议层面 MOVED 只更新对应槽 → 客户端层面做优化,主动拉取完整映射表整体替换。

这样下次再访问槽 100 的数据时,客户端就直接找新节点,不会再去旧节点碰壁。

4.3 迁移进行中:ASK 重定向

如果数据还在迁移过程中(尚未完成),旧节点会返回一个 ASK 命令:

text 复制代码
(error) ASK 100 192.168.1.2:6379

含义是:槽 100 正在迁移,你要找的数据可能在新节点上。

客户端收到 ASK 后的处理与 MOVED 完全不同:

  1. 不更新本地映射表。因为迁移还在进行中,各节点间的映射表也是动态更新的,此时更新本地映射表不准确。
  2. 向新节点发送 ASKING 命令。这一步非常关键------如果不发送 ASKING,新节点会认为这个槽还不归自己管,直接返回 MOVED,导致客户端在两个节点之间反复跳转。
  3. 在新节点上执行原命令

#mermaid-svg-uNnIGVCssi0i8nZB{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uNnIGVCssi0i8nZB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uNnIGVCssi0i8nZB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uNnIGVCssi0i8nZB .error-icon{fill:#552222;}#mermaid-svg-uNnIGVCssi0i8nZB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uNnIGVCssi0i8nZB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uNnIGVCssi0i8nZB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uNnIGVCssi0i8nZB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uNnIGVCssi0i8nZB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uNnIGVCssi0i8nZB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uNnIGVCssi0i8nZB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uNnIGVCssi0i8nZB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uNnIGVCssi0i8nZB .marker.cross{stroke:#333333;}#mermaid-svg-uNnIGVCssi0i8nZB svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uNnIGVCssi0i8nZB p{margin:0;}#mermaid-svg-uNnIGVCssi0i8nZB .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-uNnIGVCssi0i8nZB .cluster-label text{fill:#333;}#mermaid-svg-uNnIGVCssi0i8nZB .cluster-label span{color:#333;}#mermaid-svg-uNnIGVCssi0i8nZB .cluster-label span p{background-color:transparent;}#mermaid-svg-uNnIGVCssi0i8nZB .label text,#mermaid-svg-uNnIGVCssi0i8nZB span{fill:#333;color:#333;}#mermaid-svg-uNnIGVCssi0i8nZB .node rect,#mermaid-svg-uNnIGVCssi0i8nZB .node circle,#mermaid-svg-uNnIGVCssi0i8nZB .node ellipse,#mermaid-svg-uNnIGVCssi0i8nZB .node polygon,#mermaid-svg-uNnIGVCssi0i8nZB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-uNnIGVCssi0i8nZB .rough-node .label text,#mermaid-svg-uNnIGVCssi0i8nZB .node .label text,#mermaid-svg-uNnIGVCssi0i8nZB .image-shape .label,#mermaid-svg-uNnIGVCssi0i8nZB .icon-shape .label{text-anchor:middle;}#mermaid-svg-uNnIGVCssi0i8nZB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-uNnIGVCssi0i8nZB .rough-node .label,#mermaid-svg-uNnIGVCssi0i8nZB .node .label,#mermaid-svg-uNnIGVCssi0i8nZB .image-shape .label,#mermaid-svg-uNnIGVCssi0i8nZB .icon-shape .label{text-align:center;}#mermaid-svg-uNnIGVCssi0i8nZB .node.clickable{cursor:pointer;}#mermaid-svg-uNnIGVCssi0i8nZB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-uNnIGVCssi0i8nZB .arrowheadPath{fill:#333333;}#mermaid-svg-uNnIGVCssi0i8nZB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-uNnIGVCssi0i8nZB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-uNnIGVCssi0i8nZB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uNnIGVCssi0i8nZB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-uNnIGVCssi0i8nZB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uNnIGVCssi0i8nZB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-uNnIGVCssi0i8nZB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-uNnIGVCssi0i8nZB .cluster text{fill:#333;}#mermaid-svg-uNnIGVCssi0i8nZB .cluster span{color:#333;}#mermaid-svg-uNnIGVCssi0i8nZB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-uNnIGVCssi0i8nZB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-uNnIGVCssi0i8nZB rect.text{fill:none;stroke-width:0;}#mermaid-svg-uNnIGVCssi0i8nZB .icon-shape,#mermaid-svg-uNnIGVCssi0i8nZB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uNnIGVCssi0i8nZB .icon-shape p,#mermaid-svg-uNnIGVCssi0i8nZB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-uNnIGVCssi0i8nZB .icon-shape .label rect,#mermaid-svg-uNnIGVCssi0i8nZB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uNnIGVCssi0i8nZB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-uNnIGVCssi0i8nZB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-uNnIGVCssi0i8nZB :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 数据未迁移
槽已完整迁移
槽正在迁移中
客户端发送命令到旧节点
旧节点检查槽状态
旧节点直接返回结果
返回 MOVED 新节点地址
客户端更新本地映射表
客户端重新发送命令到新节点
新节点返回结果
返回 ASK 新节点地址
客户端不更新本地映射表
客户端向新节点发送 ASKING
客户端在新节点上执行原命令
新节点返回结果

下面用一张完整的流程图对比三种情况,从上到下分别对应:数据未迁移、迁移已完成、迁移进行中。关键区别在于:MOVED 会更新本地映射表,ASK 不会更新但需要先发 ASKING。

4.4 MOVED 与 ASK 的核心区别

对比项 MOVED ASK
触发时机 数据迁移已完成 数据正在迁移中
映射表状态 稳定,各节点已同步 不稳定,正在动态更新
是否更新本地映射表
是否需要 ASKING

这套机制使得 Redis Cluster 可以在不停止服务的情况下完成扩容和缩容,客户端在迁移期间也能正常访问数据。


五、数据迁移过程详解

理解了 MOVED 和 ASK 之后,我们来看数据到底是怎么迁移的。

5.1 迁移的整体流程

  1. 协调计算:协调者会进行精确计算,让所有节点的槽尽量平均分配。比如扩容了一个新节点 D,协调者决定将 Slot 5000~5460 从 Node A 迁移到 Node D。
  2. 逐槽迁移:一个槽一个槽地转移内容,而不是一次性全部搬走。
  3. 标记状态:迁移过程中,源节点和目标节点都会标记该槽为"迁移中"状态,以便正确处理 ASK 重定向。

5.2 Redis 7.0 之前:扫描全量 Key

在 Redis 7.0 之前,迁移一个槽需要:

  1. 扫描源节点上的所有 key。
  2. 对每个 key 执行 CRC16(key) % 16384
  3. 只保留结果等于目标槽号的 key,丢弃其余所有 key。
  4. 把这些 key 转移到目标节点。

#mermaid-svg-WVCsEkNTTZ4867MD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WVCsEkNTTZ4867MD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WVCsEkNTTZ4867MD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WVCsEkNTTZ4867MD .error-icon{fill:#552222;}#mermaid-svg-WVCsEkNTTZ4867MD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WVCsEkNTTZ4867MD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WVCsEkNTTZ4867MD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WVCsEkNTTZ4867MD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WVCsEkNTTZ4867MD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WVCsEkNTTZ4867MD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WVCsEkNTTZ4867MD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WVCsEkNTTZ4867MD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WVCsEkNTTZ4867MD .marker.cross{stroke:#333333;}#mermaid-svg-WVCsEkNTTZ4867MD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WVCsEkNTTZ4867MD p{margin:0;}#mermaid-svg-WVCsEkNTTZ4867MD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WVCsEkNTTZ4867MD .cluster-label text{fill:#333;}#mermaid-svg-WVCsEkNTTZ4867MD .cluster-label span{color:#333;}#mermaid-svg-WVCsEkNTTZ4867MD .cluster-label span p{background-color:transparent;}#mermaid-svg-WVCsEkNTTZ4867MD .label text,#mermaid-svg-WVCsEkNTTZ4867MD span{fill:#333;color:#333;}#mermaid-svg-WVCsEkNTTZ4867MD .node rect,#mermaid-svg-WVCsEkNTTZ4867MD .node circle,#mermaid-svg-WVCsEkNTTZ4867MD .node ellipse,#mermaid-svg-WVCsEkNTTZ4867MD .node polygon,#mermaid-svg-WVCsEkNTTZ4867MD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WVCsEkNTTZ4867MD .rough-node .label text,#mermaid-svg-WVCsEkNTTZ4867MD .node .label text,#mermaid-svg-WVCsEkNTTZ4867MD .image-shape .label,#mermaid-svg-WVCsEkNTTZ4867MD .icon-shape .label{text-anchor:middle;}#mermaid-svg-WVCsEkNTTZ4867MD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WVCsEkNTTZ4867MD .rough-node .label,#mermaid-svg-WVCsEkNTTZ4867MD .node .label,#mermaid-svg-WVCsEkNTTZ4867MD .image-shape .label,#mermaid-svg-WVCsEkNTTZ4867MD .icon-shape .label{text-align:center;}#mermaid-svg-WVCsEkNTTZ4867MD .node.clickable{cursor:pointer;}#mermaid-svg-WVCsEkNTTZ4867MD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WVCsEkNTTZ4867MD .arrowheadPath{fill:#333333;}#mermaid-svg-WVCsEkNTTZ4867MD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WVCsEkNTTZ4867MD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WVCsEkNTTZ4867MD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WVCsEkNTTZ4867MD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WVCsEkNTTZ4867MD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WVCsEkNTTZ4867MD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WVCsEkNTTZ4867MD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WVCsEkNTTZ4867MD .cluster text{fill:#333;}#mermaid-svg-WVCsEkNTTZ4867MD .cluster span{color:#333;}#mermaid-svg-WVCsEkNTTZ4867MD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WVCsEkNTTZ4867MD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WVCsEkNTTZ4867MD rect.text{fill:none;stroke-width:0;}#mermaid-svg-WVCsEkNTTZ4867MD .icon-shape,#mermaid-svg-WVCsEkNTTZ4867MD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WVCsEkNTTZ4867MD .icon-shape p,#mermaid-svg-WVCsEkNTTZ4867MD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WVCsEkNTTZ4867MD .icon-shape .label rect,#mermaid-svg-WVCsEkNTTZ4867MD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WVCsEkNTTZ4867MD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WVCsEkNTTZ4867MD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WVCsEkNTTZ4867MD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

扫描源节点所有 Key
对每个 Key 计算 CRC16 % 16384
结果等于目标槽号?
迁移到目标节点
跳过

这个方法的问题很明显:每次迁移一个槽,都要扫描全部 key 并重新计算哈希,效率很低。当节点上的 key 数量很多时,扫描和计算的开销非常大。

5.3 Redis 7.0 之后:槽位索引结构

Redis 7.0 引入了专门的槽位索引结构进行了优化:

  • 每个槽位在内存中维护了一个独立的 Key 集合(Listpack / Intset)。
  • 通过 slot_to_keys[slot_id] 这个指针,可以直接访问属于该槽的所有 Key。
  • 迁移时无需哈希计算,无需过滤,直接遍历该集合即可拿到属于目标槽的所有 Key。

#mermaid-svg-CpF2Znx4f85Efclj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CpF2Znx4f85Efclj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CpF2Znx4f85Efclj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CpF2Znx4f85Efclj .error-icon{fill:#552222;}#mermaid-svg-CpF2Znx4f85Efclj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CpF2Znx4f85Efclj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CpF2Znx4f85Efclj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CpF2Znx4f85Efclj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CpF2Znx4f85Efclj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CpF2Znx4f85Efclj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CpF2Znx4f85Efclj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CpF2Znx4f85Efclj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CpF2Znx4f85Efclj .marker.cross{stroke:#333333;}#mermaid-svg-CpF2Znx4f85Efclj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CpF2Znx4f85Efclj p{margin:0;}#mermaid-svg-CpF2Znx4f85Efclj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CpF2Znx4f85Efclj .cluster-label text{fill:#333;}#mermaid-svg-CpF2Znx4f85Efclj .cluster-label span{color:#333;}#mermaid-svg-CpF2Znx4f85Efclj .cluster-label span p{background-color:transparent;}#mermaid-svg-CpF2Znx4f85Efclj .label text,#mermaid-svg-CpF2Znx4f85Efclj span{fill:#333;color:#333;}#mermaid-svg-CpF2Znx4f85Efclj .node rect,#mermaid-svg-CpF2Znx4f85Efclj .node circle,#mermaid-svg-CpF2Znx4f85Efclj .node ellipse,#mermaid-svg-CpF2Znx4f85Efclj .node polygon,#mermaid-svg-CpF2Znx4f85Efclj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CpF2Znx4f85Efclj .rough-node .label text,#mermaid-svg-CpF2Znx4f85Efclj .node .label text,#mermaid-svg-CpF2Znx4f85Efclj .image-shape .label,#mermaid-svg-CpF2Znx4f85Efclj .icon-shape .label{text-anchor:middle;}#mermaid-svg-CpF2Znx4f85Efclj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CpF2Znx4f85Efclj .rough-node .label,#mermaid-svg-CpF2Znx4f85Efclj .node .label,#mermaid-svg-CpF2Znx4f85Efclj .image-shape .label,#mermaid-svg-CpF2Znx4f85Efclj .icon-shape .label{text-align:center;}#mermaid-svg-CpF2Znx4f85Efclj .node.clickable{cursor:pointer;}#mermaid-svg-CpF2Znx4f85Efclj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CpF2Znx4f85Efclj .arrowheadPath{fill:#333333;}#mermaid-svg-CpF2Znx4f85Efclj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CpF2Znx4f85Efclj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CpF2Znx4f85Efclj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CpF2Znx4f85Efclj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CpF2Znx4f85Efclj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CpF2Znx4f85Efclj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CpF2Znx4f85Efclj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CpF2Znx4f85Efclj .cluster text{fill:#333;}#mermaid-svg-CpF2Znx4f85Efclj .cluster span{color:#333;}#mermaid-svg-CpF2Znx4f85Efclj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CpF2Znx4f85Efclj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CpF2Znx4f85Efclj rect.text{fill:none;stroke-width:0;}#mermaid-svg-CpF2Znx4f85Efclj .icon-shape,#mermaid-svg-CpF2Znx4f85Efclj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CpF2Znx4f85Efclj .icon-shape p,#mermaid-svg-CpF2Znx4f85Efclj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CpF2Znx4f85Efclj .icon-shape .label rect,#mermaid-svg-CpF2Znx4f85Efclj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CpF2Znx4f85Efclj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CpF2Znx4f85Efclj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CpF2Znx4f85Efclj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 协调者要求迁移 Slot 5000
源节点访问 slot_to_keys 5000
直接遍历该槽的 Key 集合
将 Key 迁移到目标节点

这个优化把迁移复杂度从"扫描全部 key + 哈希计算"降低到"直接遍历目标槽的 key 集合",大幅提升了迁移效率。


六、节点之间怎么通信:Gossip 协议

解决了数据分片、客户端路由和不停机扩容之后,下一个要解决的问题是:集群中的节点之间怎么通信

节点必须知道彼此的信息------新节点加入、主从切换、节点宕机、槽在迁移------才能在故障发生时做出正确的判断。

6.1 两种通信思路:中心化 vs 去中心化

对比项 中心化 去中心化
核心思想 有一个单独的管理员角色管理整个集群 节点之间互相告知,逐步同步信息
信息传播速度 较慢
运维复杂度
典型方案 Sentinel 哨兵机制 Gossip 协议

Sentinel 用的就是中心化方案,Redis Cluster 用的则是去中心化的 Gossip 协议。

6.2 为什么 Redis Cluster 不用哨兵机制

理解了中心化和去中心化的区别之后,自然会问:为什么 Redis Cluster 不继续用哨兵机制?

哨兵机制在主从架构下用得很好,但放到 Cluster 场景下会暴露出很多问题。核心矛盾在于:Cluster 的规模和复杂度远超主从架构,中心化管理模式会扛不住。具体可以从以下几个层面理解:

第一,功能集中导致运维复杂度爆炸。

在主从架构下,Sentinel 只需要管"一个主节点挂了选谁顶上"这一件事。但到了 Cluster,Sentinel 要管的事情就多了:客户端该连哪个节点、某个槽归谁管、槽迁移怎么协调、主从切换怎么配合槽的重新分配、新节点加入怎么分槽......所有关键操作都要经过这个中心。功能一多,Sentinel 的逻辑就变得极其复杂,非常容易出错。

第二,自身高可用变成新的负担。

既然所有操作都由 Sentinel 接管,那它一定不能宕机,必须多节点部署。但 Sentinel 集群自己也需要选主、需要心跳检测、需要故障转移------相当于在 Redis 集群之上又叠了一套集群。集群一大,主从切换和槽迁移频繁触发,Sentinel 集群自身的维护开销就不小。

第三,心跳检测的网络开销随节点数线性增长,容易压垮中心节点。

Sentinel 需要对每个 Redis 节点进行心跳检测。假设有 1000 个节点,Sentinel 必须收到每个节点的回复才能判断集群状态。这意味着每轮心跳检测就要处理 1000 份回复。一旦某些节点网络延迟,回复就会堆积在 Sentinel 这里。节点越多,堆积越严重,最终 Sentinel 自己就被压垮了------这和它要保护的 Redis 集群一样,成了新的瓶颈。

第四,故障切换延迟高,效果不一定好。

中心化方案下,所有决策都要经过 Sentinel 中转。当集群节点很多时,Sentinel 收集信息、做出判断、下发指令的整个链路变长,故障转移的延迟也随之增加。而去中心化方案让节点之间直接协商,反而更快。

正因为这些问题,Redis Cluster 选择了去中心化的 Gossip 协议。

6.3 Gossip 协议的工作机制

Gossip 协议的核心思想很简单:
每个节点周期性地随机挑选几个邻居节点,把自己知道的消息告诉对方。每个节点都这样做,消息就会像传染病一样扩散开来,最终所有节点都知道了集群的完整信息。
#mermaid-svg-Ce9GUnwkRjZCALBy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Ce9GUnwkRjZCALBy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Ce9GUnwkRjZCALBy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Ce9GUnwkRjZCALBy .error-icon{fill:#552222;}#mermaid-svg-Ce9GUnwkRjZCALBy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Ce9GUnwkRjZCALBy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Ce9GUnwkRjZCALBy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Ce9GUnwkRjZCALBy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Ce9GUnwkRjZCALBy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Ce9GUnwkRjZCALBy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Ce9GUnwkRjZCALBy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Ce9GUnwkRjZCALBy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Ce9GUnwkRjZCALBy .marker.cross{stroke:#333333;}#mermaid-svg-Ce9GUnwkRjZCALBy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Ce9GUnwkRjZCALBy p{margin:0;}#mermaid-svg-Ce9GUnwkRjZCALBy .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Ce9GUnwkRjZCALBy .cluster-label text{fill:#333;}#mermaid-svg-Ce9GUnwkRjZCALBy .cluster-label span{color:#333;}#mermaid-svg-Ce9GUnwkRjZCALBy .cluster-label span p{background-color:transparent;}#mermaid-svg-Ce9GUnwkRjZCALBy .label text,#mermaid-svg-Ce9GUnwkRjZCALBy span{fill:#333;color:#333;}#mermaid-svg-Ce9GUnwkRjZCALBy .node rect,#mermaid-svg-Ce9GUnwkRjZCALBy .node circle,#mermaid-svg-Ce9GUnwkRjZCALBy .node ellipse,#mermaid-svg-Ce9GUnwkRjZCALBy .node polygon,#mermaid-svg-Ce9GUnwkRjZCALBy .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ce9GUnwkRjZCALBy .rough-node .label text,#mermaid-svg-Ce9GUnwkRjZCALBy .node .label text,#mermaid-svg-Ce9GUnwkRjZCALBy .image-shape .label,#mermaid-svg-Ce9GUnwkRjZCALBy .icon-shape .label{text-anchor:middle;}#mermaid-svg-Ce9GUnwkRjZCALBy .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Ce9GUnwkRjZCALBy .rough-node .label,#mermaid-svg-Ce9GUnwkRjZCALBy .node .label,#mermaid-svg-Ce9GUnwkRjZCALBy .image-shape .label,#mermaid-svg-Ce9GUnwkRjZCALBy .icon-shape .label{text-align:center;}#mermaid-svg-Ce9GUnwkRjZCALBy .node.clickable{cursor:pointer;}#mermaid-svg-Ce9GUnwkRjZCALBy .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Ce9GUnwkRjZCALBy .arrowheadPath{fill:#333333;}#mermaid-svg-Ce9GUnwkRjZCALBy .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Ce9GUnwkRjZCALBy .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Ce9GUnwkRjZCALBy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ce9GUnwkRjZCALBy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Ce9GUnwkRjZCALBy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ce9GUnwkRjZCALBy .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Ce9GUnwkRjZCALBy .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Ce9GUnwkRjZCALBy .cluster text{fill:#333;}#mermaid-svg-Ce9GUnwkRjZCALBy .cluster span{color:#333;}#mermaid-svg-Ce9GUnwkRjZCALBy div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Ce9GUnwkRjZCALBy .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Ce9GUnwkRjZCALBy rect.text{fill:none;stroke-width:0;}#mermaid-svg-Ce9GUnwkRjZCALBy .icon-shape,#mermaid-svg-Ce9GUnwkRjZCALBy .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ce9GUnwkRjZCALBy .icon-shape p,#mermaid-svg-Ce9GUnwkRjZCALBy .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Ce9GUnwkRjZCALBy .icon-shape .label rect,#mermaid-svg-Ce9GUnwkRjZCALBy .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ce9GUnwkRjZCALBy .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Ce9GUnwkRjZCALBy .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Ce9GUnwkRjZCALBy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 随机挑选邻居
随机挑选邻居
下一次周期
下一次周期
下一次周期
下一次周期
Node A 知道新信息
Node B
Node C
Node D
Node E
Node F

这个机制的优点是运维简单,不需要额外部署管理组件;缺点是消息传播有一定延迟,但这个延迟在集群规模可控的情况下是可以接受的。

6.4 四种消息类型

Gossip 协议规定了四种消息类型:

1. PING

节点每秒都会向其他节点发送 PING,里面附带自己知道的集群状态------哪些节点是活的、哪些槽属于哪些节点。这是信息传播的主要载体。

2. PONG

确认信息。接收到 PING 的节点会回复 PONG,告知对方"我收到了,我还活着"。

3. MEET

当有新节点加入集群时,集群中的一个老节点会向新节点发送 MEET。新节点收到后与老节点建立连接,然后开始发送 PING,实现传播式更新。当新节点通过 PING 发现了其他未连接的老节点,会主动与它们建立连接,最终让整个集群的每个节点都知道新节点的存在。

4. FAIL

当某个节点被判定为彻底挂了,会有节点向整个集群广播 FAIL 消息,告诉大家"这家伙真的不行了,把它的状态更新成下线吧"。

6.5 集群总线端口

节点之间交换 Gossip 消息用的不是 6379 端口。6379 是客户端连接的业务端口,集群有一个专门的集群总线端口(Cluster Bus)

约定规则是:业务端口 + 10000。如果业务端口是 6379,那么集群总线端口就是 16379。

这意味着部署 Redis Cluster 时,除了开放业务端口,还需要开放对应的集群总线端口,否则节点之间无法通信,集群无法搭建。

6.6 集群规模上限:为什么不超过 1000 个主节点

官方建议 Redis Cluster 的主节点不要超过 1000 个。虽然 Gossip 的去中心化方案减小了运维难度,但集群一大,每秒发送的 PING 携带的信息就多,网络开销随之增大。1000 个节点相互打招呼,网络开销已经不小。不过在 1000 个节点规模下,Gossip 仍然是可控的------每个节点只和少数几个邻居通信,不会因为集群规模增大而出现单点瓶颈。


七、怎么实现故障转移

理解了节点间通信之后,就可以看故障转移了。

在 Redis Cluster 中,从节点默认不提供读写服务,只作为主节点的数据备份。一旦主节点挂了,从节点直接顶上去。

为什么从节点默认不提供读写服务?因为主从复制是异步的,从节点的数据可能滞后于主节点。如果允许从节点处理读请求,客户端就可能读到旧数据。在 Cluster 中这个问题更敏感------数据已经被分散到多个分片,如果某个分片的从节点返回了过期数据,业务层很难察觉。所以默认情况下,所有读写都走主节点,保证读到的是最新数据。至于从节点,它的唯一职责就是随时准备在主节点故障时顶上去。

故障转移分为两个阶段:故障发现故障恢复

7.1 故障发现:从主观下线到客观下线

主观下线(PFAIL)

节点之间每秒互相发送 PING。如果某个节点在 cluster-node-timeout(默认 15 秒)内没有收到对方的回复,就会把对方标记为 PFAIL(Possibly Failing),即主观下线。

然后在下一次 PING 时,把这个 PFAIL 信息一起携带给附近的节点。

客观下线(FAIL)

这里有一个关键问题:在哨兵机制中,哨兵会主动记录投票数。但集群里面没有哨兵,只有认为某个节点客观下线时才会广播 FAIL 消息。那这个投票数是怎么记录的?

答案是:每个节点都会维护一张"下线报告表",并在每次 PING 时把自己这张表的内容带给对方,对方收到后再更新自己的表。这就实现了投票信息的扩散。

用具体例子来说明整个过程:

假设集群有 5 个主节点 A、B、C、D、E,其中 Node A 真的挂了。

第一步:Node B 发现 Node A 没回应。

Node B 每秒向 Node A 发 PING,超过 cluster-node-timeout(默认 15 秒)没收到回复,就在自己的下线报告表里记录:

复制代码
Node B 的下线报告表:
  Node A → [Node B 认为它下线(PFAIL)]

第二步:Node B 在下一次 PING 时把这个信息带给 Node C。

Node B 向 Node C 发送 PING,消息里携带了自己的下线报告表。Node C 收到后,更新自己的表:

复制代码
Node C 的下线报告表:
  Node A → [Node B 认为它下线(PFAIL)]

注意:Node C 只是记录了"Node B 认为 Node A 下线"这件事,不会主动去 PING 一下 Node A 验证。原因后面讲。

第三步:Node D 也发现 Node A 没回应,同时收到了 Node C 的报告。

Node D 自己 PING Node A 也超时了,在表里加上自己的投票。同时它收到了 Node C 的 PING,把 Node B 的投票也记进来:

复制代码
Node D 的下线报告表:
  Node A → [Node B 认为下线, Node C 认为下线, Node D 认为下线]

第四步:投票数过半,广播 FAIL。

当某个节点的下线报告表中,认为 Node A 下线的节点数量超过了半数(这里是 3 个,超过 5/2=2.5),就判定 Node A 客观下线,向整个集群广播 FAIL 消息。

回到前面那个关键点:为什么收到 PFAIL 信息的节点不主动去验证?

因为如果每个收到"Node A 可能下线"消息的节点都主动去 PING 一次 Node A,就会在网络上引发消息风暴。假设集群有 1000 个节点,其中 500 个节点都收到了这个消息,就会瞬间多出 500 次 PING。集群越大,风暴越夸张,远超正常的网络开销。所以节点只是单纯地记录,不做额外探测,只等自己正常的 PING 轮到 Node A 时自然去判断。
#mermaid-svg-koRgLTVH7WRtgPTj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-koRgLTVH7WRtgPTj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-koRgLTVH7WRtgPTj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-koRgLTVH7WRtgPTj .error-icon{fill:#552222;}#mermaid-svg-koRgLTVH7WRtgPTj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-koRgLTVH7WRtgPTj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-koRgLTVH7WRtgPTj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-koRgLTVH7WRtgPTj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-koRgLTVH7WRtgPTj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-koRgLTVH7WRtgPTj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-koRgLTVH7WRtgPTj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-koRgLTVH7WRtgPTj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-koRgLTVH7WRtgPTj .marker.cross{stroke:#333333;}#mermaid-svg-koRgLTVH7WRtgPTj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-koRgLTVH7WRtgPTj p{margin:0;}#mermaid-svg-koRgLTVH7WRtgPTj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-koRgLTVH7WRtgPTj .cluster-label text{fill:#333;}#mermaid-svg-koRgLTVH7WRtgPTj .cluster-label span{color:#333;}#mermaid-svg-koRgLTVH7WRtgPTj .cluster-label span p{background-color:transparent;}#mermaid-svg-koRgLTVH7WRtgPTj .label text,#mermaid-svg-koRgLTVH7WRtgPTj span{fill:#333;color:#333;}#mermaid-svg-koRgLTVH7WRtgPTj .node rect,#mermaid-svg-koRgLTVH7WRtgPTj .node circle,#mermaid-svg-koRgLTVH7WRtgPTj .node ellipse,#mermaid-svg-koRgLTVH7WRtgPTj .node polygon,#mermaid-svg-koRgLTVH7WRtgPTj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-koRgLTVH7WRtgPTj .rough-node .label text,#mermaid-svg-koRgLTVH7WRtgPTj .node .label text,#mermaid-svg-koRgLTVH7WRtgPTj .image-shape .label,#mermaid-svg-koRgLTVH7WRtgPTj .icon-shape .label{text-anchor:middle;}#mermaid-svg-koRgLTVH7WRtgPTj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-koRgLTVH7WRtgPTj .rough-node .label,#mermaid-svg-koRgLTVH7WRtgPTj .node .label,#mermaid-svg-koRgLTVH7WRtgPTj .image-shape .label,#mermaid-svg-koRgLTVH7WRtgPTj .icon-shape .label{text-align:center;}#mermaid-svg-koRgLTVH7WRtgPTj .node.clickable{cursor:pointer;}#mermaid-svg-koRgLTVH7WRtgPTj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-koRgLTVH7WRtgPTj .arrowheadPath{fill:#333333;}#mermaid-svg-koRgLTVH7WRtgPTj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-koRgLTVH7WRtgPTj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-koRgLTVH7WRtgPTj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-koRgLTVH7WRtgPTj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-koRgLTVH7WRtgPTj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-koRgLTVH7WRtgPTj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-koRgLTVH7WRtgPTj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-koRgLTVH7WRtgPTj .cluster text{fill:#333;}#mermaid-svg-koRgLTVH7WRtgPTj .cluster span{color:#333;}#mermaid-svg-koRgLTVH7WRtgPTj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-koRgLTVH7WRtgPTj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-koRgLTVH7WRtgPTj rect.text{fill:none;stroke-width:0;}#mermaid-svg-koRgLTVH7WRtgPTj .icon-shape,#mermaid-svg-koRgLTVH7WRtgPTj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-koRgLTVH7WRtgPTj .icon-shape p,#mermaid-svg-koRgLTVH7WRtgPTj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-koRgLTVH7WRtgPTj .icon-shape .label rect,#mermaid-svg-koRgLTVH7WRtgPTj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-koRgLTVH7WRtgPTj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-koRgLTVH7WRtgPTj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-koRgLTVH7WRtgPTj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是


节点间每秒 PING
某节点 cluster-node-timeout 内未回复
自己标记为 PFAIL
在下线报告表中记录自己的投票
下次 PING 时把下线报告表带给对方
对方收到后更新自己的下线报告表
不做额外探测,避免消息风暴
下线报告数超过半数?
继续监控,等自己正常 PING 时再判断
广播 FAIL 消息,判定客观下线

7.2 故障恢复:类似 Raft 的投票选主

广播 FAIL 之后,就开始真正的故障转移。这里采取的是类似 Raft 的投票机制,大致分为四步:

第一步:资格检查

不是所有从节点都有资格参与选举。对于很久未与主节点同步的从节点会被排除。判断标准是配置项 cluster-replica-validity-factor,如果从节点的数据落后太多,就不具备参选资格。

第二步:按数据同步进度排队

通过资格检查的从节点,会根据自己的数据同步进度进行排队。同步进度越靠前的,排名越靠前。

第三步:按顺序拉票

排在第一位的从节点会向所有拥有槽的主节点发送拉票通知。这个机制很巧妙,我们需要理解它为什么能选出"数据丢失最少、网络最通畅"的节点:

为什么数据丢失最少? 因为排队依据是数据同步进度。同步进度靠前,说明这个从节点已经收到了主节点最近的大部分写入。选它当新主,主节点故障前还没来得及同步的那部分数据就最少,数据丢失自然最少。

为什么网络最通畅? 因为只有同步进度靠前的从节点才有资格先拉票。如果某个从节点网络不好、和主节点的同步长期滞后,它的同步进度就会靠后,排在后面,只有前面的从节点都失败(比如没拿到半数票)才会轮到它。这等于自动过滤掉了网络不稳定的节点。

所以这个机制同时兼顾了"数据完整性"和"网络稳定性"两个维度。

第四步:收集选票并当选

投票规则是:

  • 只有拥有槽的主节点可以投票
  • 每个主节点只能投一票。投了这个从节点后,就不能再投给其他从节点。

如果某个从节点的票数超过半数,它就当选为新的主节点。
#mermaid-svg-ZYLkxjcexIa0Xium{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZYLkxjcexIa0Xium .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZYLkxjcexIa0Xium .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZYLkxjcexIa0Xium .error-icon{fill:#552222;}#mermaid-svg-ZYLkxjcexIa0Xium .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZYLkxjcexIa0Xium .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZYLkxjcexIa0Xium .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZYLkxjcexIa0Xium .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZYLkxjcexIa0Xium .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZYLkxjcexIa0Xium .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZYLkxjcexIa0Xium .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZYLkxjcexIa0Xium .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZYLkxjcexIa0Xium .marker.cross{stroke:#333333;}#mermaid-svg-ZYLkxjcexIa0Xium svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZYLkxjcexIa0Xium p{margin:0;}#mermaid-svg-ZYLkxjcexIa0Xium .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ZYLkxjcexIa0Xium .cluster-label text{fill:#333;}#mermaid-svg-ZYLkxjcexIa0Xium .cluster-label span{color:#333;}#mermaid-svg-ZYLkxjcexIa0Xium .cluster-label span p{background-color:transparent;}#mermaid-svg-ZYLkxjcexIa0Xium .label text,#mermaid-svg-ZYLkxjcexIa0Xium span{fill:#333;color:#333;}#mermaid-svg-ZYLkxjcexIa0Xium .node rect,#mermaid-svg-ZYLkxjcexIa0Xium .node circle,#mermaid-svg-ZYLkxjcexIa0Xium .node ellipse,#mermaid-svg-ZYLkxjcexIa0Xium .node polygon,#mermaid-svg-ZYLkxjcexIa0Xium .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZYLkxjcexIa0Xium .rough-node .label text,#mermaid-svg-ZYLkxjcexIa0Xium .node .label text,#mermaid-svg-ZYLkxjcexIa0Xium .image-shape .label,#mermaid-svg-ZYLkxjcexIa0Xium .icon-shape .label{text-anchor:middle;}#mermaid-svg-ZYLkxjcexIa0Xium .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ZYLkxjcexIa0Xium .rough-node .label,#mermaid-svg-ZYLkxjcexIa0Xium .node .label,#mermaid-svg-ZYLkxjcexIa0Xium .image-shape .label,#mermaid-svg-ZYLkxjcexIa0Xium .icon-shape .label{text-align:center;}#mermaid-svg-ZYLkxjcexIa0Xium .node.clickable{cursor:pointer;}#mermaid-svg-ZYLkxjcexIa0Xium .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ZYLkxjcexIa0Xium .arrowheadPath{fill:#333333;}#mermaid-svg-ZYLkxjcexIa0Xium .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ZYLkxjcexIa0Xium .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ZYLkxjcexIa0Xium .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZYLkxjcexIa0Xium .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ZYLkxjcexIa0Xium .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZYLkxjcexIa0Xium .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ZYLkxjcexIa0Xium .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZYLkxjcexIa0Xium .cluster text{fill:#333;}#mermaid-svg-ZYLkxjcexIa0Xium .cluster span{color:#333;}#mermaid-svg-ZYLkxjcexIa0Xium div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ZYLkxjcexIa0Xium .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZYLkxjcexIa0Xium rect.text{fill:none;stroke-width:0;}#mermaid-svg-ZYLkxjcexIa0Xium .icon-shape,#mermaid-svg-ZYLkxjcexIa0Xium .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ZYLkxjcexIa0Xium .icon-shape p,#mermaid-svg-ZYLkxjcexIa0Xium .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ZYLkxjcexIa0Xium .icon-shape .label rect,#mermaid-svg-ZYLkxjcexIa0Xium .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ZYLkxjcexIa0Xium .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZYLkxjcexIa0Xium .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZYLkxjcexIa0Xium :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 排除不合规节点


FAIL 消息广播
从节点资格检查
按同步进度排队
排名第一的从节点拉票
向所有拥有槽的主节点请求投票
票数超过半数?
当选为新主节点
等待后重新投票
广播当选消息
更新槽与节点映射表
客户端收到 MOVED 更新本地映射表

新主节点当选后,会广播一条消息告知整个集群,然后更新各个节点的本地槽与节点映射表。这样下次客户端发送命令时,就会被 MOVED 重定向到新主节点,客户端也会更新自己的本地映射表。

7.3 故障转移的极端情况

选票未过半: 如果因为网络状况导致选票丢失,没有一个从节点的票数超过半数,就会进行合理的停滞之后再次重新投票。这和 Raft 的选举超时重试机制类似。

迁移期间命令被拒绝: 在主从切换期间,相关槽的命令会被拒绝。

数据丢失风险: Redis 主从同步是异步复制,所以极端情况下可能出现数据丢失------主节点处理完命令、回复 OK 给客户端之后,还没来得及同步给从节点就挂了。这种概率很低,但确实存在,这是异步复制机制的一定取舍。


八、多 key 操作与读写分离

8.1 多 key 命令的问题

在单节点 Redis 上,多 key 操作没有问题:

bash 复制代码
MSET user:1:name xiaolin user:1:age 18

但在 Redis Cluster 中,这两个 key 可能被分配到不同的槽、不同的节点。而客户端只会通过一个 pipeline 把命令传到一个节点,所以这种跨节点的多 key 命令会直接报错。

8.2 解决方案:Hash Tag

Redis Cluster 提供了 Hash Tag 机制来解决这个问题。在 key 中使用花括号 {} 括起一部分内容,只有花括号内的内容会参与 CRC16 运算和取余

bash 复制代码
MSET {user:1}:name xiaolin {user:1}:age 18

在这个例子中,user:1 是参与哈希运算的部分,而 :name:age 不参与。因此两个 key 会算出相同的槽号,落入同一个节点,多 key 操作就可以正常执行了。
#mermaid-svg-e8MnqmAQbRK2NMdj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-e8MnqmAQbRK2NMdj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-e8MnqmAQbRK2NMdj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-e8MnqmAQbRK2NMdj .error-icon{fill:#552222;}#mermaid-svg-e8MnqmAQbRK2NMdj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-e8MnqmAQbRK2NMdj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-e8MnqmAQbRK2NMdj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-e8MnqmAQbRK2NMdj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-e8MnqmAQbRK2NMdj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-e8MnqmAQbRK2NMdj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-e8MnqmAQbRK2NMdj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-e8MnqmAQbRK2NMdj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-e8MnqmAQbRK2NMdj .marker.cross{stroke:#333333;}#mermaid-svg-e8MnqmAQbRK2NMdj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-e8MnqmAQbRK2NMdj p{margin:0;}#mermaid-svg-e8MnqmAQbRK2NMdj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-e8MnqmAQbRK2NMdj .cluster-label text{fill:#333;}#mermaid-svg-e8MnqmAQbRK2NMdj .cluster-label span{color:#333;}#mermaid-svg-e8MnqmAQbRK2NMdj .cluster-label span p{background-color:transparent;}#mermaid-svg-e8MnqmAQbRK2NMdj .label text,#mermaid-svg-e8MnqmAQbRK2NMdj span{fill:#333;color:#333;}#mermaid-svg-e8MnqmAQbRK2NMdj .node rect,#mermaid-svg-e8MnqmAQbRK2NMdj .node circle,#mermaid-svg-e8MnqmAQbRK2NMdj .node ellipse,#mermaid-svg-e8MnqmAQbRK2NMdj .node polygon,#mermaid-svg-e8MnqmAQbRK2NMdj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-e8MnqmAQbRK2NMdj .rough-node .label text,#mermaid-svg-e8MnqmAQbRK2NMdj .node .label text,#mermaid-svg-e8MnqmAQbRK2NMdj .image-shape .label,#mermaid-svg-e8MnqmAQbRK2NMdj .icon-shape .label{text-anchor:middle;}#mermaid-svg-e8MnqmAQbRK2NMdj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-e8MnqmAQbRK2NMdj .rough-node .label,#mermaid-svg-e8MnqmAQbRK2NMdj .node .label,#mermaid-svg-e8MnqmAQbRK2NMdj .image-shape .label,#mermaid-svg-e8MnqmAQbRK2NMdj .icon-shape .label{text-align:center;}#mermaid-svg-e8MnqmAQbRK2NMdj .node.clickable{cursor:pointer;}#mermaid-svg-e8MnqmAQbRK2NMdj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-e8MnqmAQbRK2NMdj .arrowheadPath{fill:#333333;}#mermaid-svg-e8MnqmAQbRK2NMdj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-e8MnqmAQbRK2NMdj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-e8MnqmAQbRK2NMdj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-e8MnqmAQbRK2NMdj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-e8MnqmAQbRK2NMdj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-e8MnqmAQbRK2NMdj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-e8MnqmAQbRK2NMdj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-e8MnqmAQbRK2NMdj .cluster text{fill:#333;}#mermaid-svg-e8MnqmAQbRK2NMdj .cluster span{color:#333;}#mermaid-svg-e8MnqmAQbRK2NMdj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-e8MnqmAQbRK2NMdj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-e8MnqmAQbRK2NMdj rect.text{fill:none;stroke-width:0;}#mermaid-svg-e8MnqmAQbRK2NMdj .icon-shape,#mermaid-svg-e8MnqmAQbRK2NMdj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-e8MnqmAQbRK2NMdj .icon-shape p,#mermaid-svg-e8MnqmAQbRK2NMdj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-e8MnqmAQbRK2NMdj .icon-shape .label rect,#mermaid-svg-e8MnqmAQbRK2NMdj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-e8MnqmAQbRK2NMdj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-e8MnqmAQbRK2NMdj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-e8MnqmAQbRK2NMdj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 使用 Hash Tag
只对 user:1 做 CRC16
只对 user:1 做 CRC16
{user:1}:name
槽 3000 → 节点 A
{user:1}:age
槽 3000 → 节点 A
同一节点,MSET 成功
不使用 Hash Tag
CRC16
CRC16
user:1:name
槽 5460 → 节点 A
user:1:age
槽 10922 → 节点 B
不同节点,MSET 报错

8.3 从节点默认不读写分离

在 Redis Cluster 中,从节点默认不提供读写服务。为什么不默认采取读写分离?主要是一致性问题。

主从复制是异步的,从节点的数据可能滞后于主节点。所以:

  • 强一致性数据(如库存、金额等)最好不要从从节点读取,否则可能引发业务问题。
  • 弱一致性数据(如商品详情页等不需要强一致的信息)可以采取读写分离,只需要最终一致即可。

如果需要开启从节点读取,客户端需要先向从节点发送一个 READONLY 命令:

bash 复制代码
READONLY

发送后,后续的读请求就会在从节点上正常处理,不会返回 MOVED 重定向到主节点。


九、为什么是 16384 个槽

最后回答一个常见的问题:为什么 Redis Cluster 的槽数量是 16384,而不是更多或更少?

9.1 网络开销:每次 PING 携带 2KB

集群中会进行大量的 PING 通信,信息传输速度必须快。在 PING 消息中,每个节点管理的槽范围是通过**位图(bitmap)**实现的,一个槽占一个 bit:

  • 管理的槽记为 1
  • 不管的槽记为 0

16384 bit ÷ 8 = 2048 字节 = 2 KB

每次 PING 携带 2KB 的位图信息,网络开销是可接受的。

9.2 负载均衡:1000 个节点每人约 16 个槽

为什么不选更小的数字(比如 8192)?因为 Redis Cluster 的建议规模上限是 1000 个主节点。

  • 16384 个槽 ÷ 1000 个节点 ≈ 每个节点 16 个槽,足以保证负载均衡。
  • 8192 个槽 ÷ 1000 个节点 ≈ 每个节点 8 个槽,略显紧张。

9.3 位运算优化:16384 是 2 的 14 次方

还有一个性能上的考量。计算槽位时使用的是位运算:

text 复制代码
slot = CRC16(key) & 16383

因为 x % (2^n) 等价于 x & (2^n - 1),而 16384 = 2^14,16383 = 2^14 - 1。位运算 &% 取模运算更快。这也是为什么槽的数量要选一个 2 的幂次方。


十、总结

本文围绕 Redis Cluster 的六大核心问题展开,可以串联成一条完整的主线:

  1. 容量瓶颈:主从+哨兵解决了读写扩展和故障转移,但单机内存有上限,数据量超过一定阈值后 fork 和 RDB 生成会拖慢主线程。
  2. 数据分片:通过 16384 个哈希槽实现 Key 到槽的固定映射,再由槽映射到节点。扩缩容只需调整槽与节点的映射,不影响 Key 的归属。
  3. 客户端路由:客户端本地缓存槽与节点的映射表,直接计算 key 的槽号并发送到对应节点,无需代理层。
  4. 不停机扩容:通过 MOVED(迁移完成,更新映射表)和 ASK(迁移中,临时重定向)实现平滑扩缩容。Redis 7.0 通过槽位索引结构优化了迁移效率。
  5. 节点通信:采用去中心化的 Gossip 协议(PING/PONG/MEET/FAIL),避免中心化管理带来的运维复杂度和单点瓶颈。
  6. 故障转移:通过 PFAIL 本本记录实现客观下线判定,再通过类似 Raft 的投票机制选出数据最新的从节点升主。
  7. 多 key 处理:通过 Hash Tag 让多个 key 落入同一槽位;从节点默认不读写分离,需要时通过 READONLY 开启。

用一句话概括:Redis Cluster 通过哈希槽分片突破单机容量限制,通过 MOVED/ASK 重定向实现不停机扩容,通过 Gossip 协议实现去中心化通信,通过类 Raft 投票实现自动故障转移,最终在性能、容量和可用性之间找到了平衡。

相关推荐
老孙讲技术2 小时前
巡店系统笔记:listDeviceDetailsByPage 台账 + bindDeviceLive 辅码流最小闭环
后端·物联网
Dong先生2 小时前
Harness vibecoding:从 PRD 到可交付代码的结构化工法
后端
蓝胖的四次元口袋2 小时前
JavaString知识梳理
数据库·oracle
雪隐3 小时前
用Flutter做背单词APP-03为了给单词库塞满数据,我让AI给我打了上万份工
前端·人工智能·后端
csdn_aspnet3 小时前
mysql 使用逗号拼接一列数据
数据库·mysql·group_concat
苏三说技术3 小时前
为什么越来越多的人使用PostgreSQL?
后端
Csvn3 小时前
📊 SQL 入门 Day 1:SELECT & WHERE — 数据查询的基本功
后端·sql
不一样的少年_4 小时前
不用框架,手搓 AI Agent:(一) 先让它跑起来
前端·后端·agent
大师兄66684 小时前
HarmonyOS7 数据持久化 relationalStore:数据库实战
数据库·arkui·实战案例·harmonyos7