Redis 从入门到精通(六):集群模式(Cluster)------ 分布式架构、哈希槽与 Gossip 协议全解
一、为什么需要 Cluster?从单机到分片的演进
1.1 单机 → 主从 → Sentinel → Cluster 的演进路径
#mermaid-svg-G9NC7JOagu7wtqU7{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-G9NC7JOagu7wtqU7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-G9NC7JOagu7wtqU7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-G9NC7JOagu7wtqU7 .error-icon{fill:#552222;}#mermaid-svg-G9NC7JOagu7wtqU7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-G9NC7JOagu7wtqU7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-G9NC7JOagu7wtqU7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-G9NC7JOagu7wtqU7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-G9NC7JOagu7wtqU7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-G9NC7JOagu7wtqU7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-G9NC7JOagu7wtqU7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-G9NC7JOagu7wtqU7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-G9NC7JOagu7wtqU7 .marker.cross{stroke:#333333;}#mermaid-svg-G9NC7JOagu7wtqU7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-G9NC7JOagu7wtqU7 p{margin:0;}#mermaid-svg-G9NC7JOagu7wtqU7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-G9NC7JOagu7wtqU7 .cluster-label text{fill:#333;}#mermaid-svg-G9NC7JOagu7wtqU7 .cluster-label span{color:#333;}#mermaid-svg-G9NC7JOagu7wtqU7 .cluster-label span p{background-color:transparent;}#mermaid-svg-G9NC7JOagu7wtqU7 .label text,#mermaid-svg-G9NC7JOagu7wtqU7 span{fill:#333;color:#333;}#mermaid-svg-G9NC7JOagu7wtqU7 .node rect,#mermaid-svg-G9NC7JOagu7wtqU7 .node circle,#mermaid-svg-G9NC7JOagu7wtqU7 .node ellipse,#mermaid-svg-G9NC7JOagu7wtqU7 .node polygon,#mermaid-svg-G9NC7JOagu7wtqU7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-G9NC7JOagu7wtqU7 .rough-node .label text,#mermaid-svg-G9NC7JOagu7wtqU7 .node .label text,#mermaid-svg-G9NC7JOagu7wtqU7 .image-shape .label,#mermaid-svg-G9NC7JOagu7wtqU7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-G9NC7JOagu7wtqU7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-G9NC7JOagu7wtqU7 .rough-node .label,#mermaid-svg-G9NC7JOagu7wtqU7 .node .label,#mermaid-svg-G9NC7JOagu7wtqU7 .image-shape .label,#mermaid-svg-G9NC7JOagu7wtqU7 .icon-shape .label{text-align:center;}#mermaid-svg-G9NC7JOagu7wtqU7 .node.clickable{cursor:pointer;}#mermaid-svg-G9NC7JOagu7wtqU7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-G9NC7JOagu7wtqU7 .arrowheadPath{fill:#333333;}#mermaid-svg-G9NC7JOagu7wtqU7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-G9NC7JOagu7wtqU7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-G9NC7JOagu7wtqU7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-G9NC7JOagu7wtqU7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-G9NC7JOagu7wtqU7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-G9NC7JOagu7wtqU7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-G9NC7JOagu7wtqU7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-G9NC7JOagu7wtqU7 .cluster text{fill:#333;}#mermaid-svg-G9NC7JOagu7wtqU7 .cluster span{color:#333;}#mermaid-svg-G9NC7JOagu7wtqU7 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-G9NC7JOagu7wtqU7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-G9NC7JOagu7wtqU7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-G9NC7JOagu7wtqU7 .icon-shape,#mermaid-svg-G9NC7JOagu7wtqU7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-G9NC7JOagu7wtqU7 .icon-shape p,#mermaid-svg-G9NC7JOagu7wtqU7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-G9NC7JOagu7wtqU7 .icon-shape .label rect,#mermaid-svg-G9NC7JOagu7wtqU7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-G9NC7JOagu7wtqU7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-G9NC7JOagu7wtqU7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-G9NC7JOagu7wtqU7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 主从复制
Sentinel
Cluster
单机 Redis
容量=单机内存
无高可用
主从模式
读写分离
手动故障转移
Sentinel 高可用
自动故障转移
容量=单机内存
Redis Cluster
数据分片
去中心化
容量 = N × 单机内存
| 阶段 | 解决了什么 | 没解决什么 |
|---|---|---|
| 单机 | 能用 | 容量瓶颈、单点故障、读压力 |
| 主从 | 读写分离、多副本 | 容量瓶颈、手动切换 |
| Sentinel | 自动故障转移 | 容量瓶颈依然存在 |
| Cluster | 数据分片、水平扩展 | 复杂度提升、跨槽操作受限 |
一句话 :Sentinel 解决的是高可用问题,Cluster 解决的是**分布式(容量 + 高可用)**问题。
1.2 Cluster 的核心设计理念
#mermaid-svg-m6tMN0QZ2Q09VtlT{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-m6tMN0QZ2Q09VtlT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-m6tMN0QZ2Q09VtlT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-m6tMN0QZ2Q09VtlT .error-icon{fill:#552222;}#mermaid-svg-m6tMN0QZ2Q09VtlT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-m6tMN0QZ2Q09VtlT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-m6tMN0QZ2Q09VtlT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-m6tMN0QZ2Q09VtlT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-m6tMN0QZ2Q09VtlT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-m6tMN0QZ2Q09VtlT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-m6tMN0QZ2Q09VtlT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-m6tMN0QZ2Q09VtlT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-m6tMN0QZ2Q09VtlT .marker.cross{stroke:#333333;}#mermaid-svg-m6tMN0QZ2Q09VtlT svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-m6tMN0QZ2Q09VtlT p{margin:0;}#mermaid-svg-m6tMN0QZ2Q09VtlT .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-m6tMN0QZ2Q09VtlT .cluster-label text{fill:#333;}#mermaid-svg-m6tMN0QZ2Q09VtlT .cluster-label span{color:#333;}#mermaid-svg-m6tMN0QZ2Q09VtlT .cluster-label span p{background-color:transparent;}#mermaid-svg-m6tMN0QZ2Q09VtlT .label text,#mermaid-svg-m6tMN0QZ2Q09VtlT span{fill:#333;color:#333;}#mermaid-svg-m6tMN0QZ2Q09VtlT .node rect,#mermaid-svg-m6tMN0QZ2Q09VtlT .node circle,#mermaid-svg-m6tMN0QZ2Q09VtlT .node ellipse,#mermaid-svg-m6tMN0QZ2Q09VtlT .node polygon,#mermaid-svg-m6tMN0QZ2Q09VtlT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-m6tMN0QZ2Q09VtlT .rough-node .label text,#mermaid-svg-m6tMN0QZ2Q09VtlT .node .label text,#mermaid-svg-m6tMN0QZ2Q09VtlT .image-shape .label,#mermaid-svg-m6tMN0QZ2Q09VtlT .icon-shape .label{text-anchor:middle;}#mermaid-svg-m6tMN0QZ2Q09VtlT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-m6tMN0QZ2Q09VtlT .rough-node .label,#mermaid-svg-m6tMN0QZ2Q09VtlT .node .label,#mermaid-svg-m6tMN0QZ2Q09VtlT .image-shape .label,#mermaid-svg-m6tMN0QZ2Q09VtlT .icon-shape .label{text-align:center;}#mermaid-svg-m6tMN0QZ2Q09VtlT .node.clickable{cursor:pointer;}#mermaid-svg-m6tMN0QZ2Q09VtlT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-m6tMN0QZ2Q09VtlT .arrowheadPath{fill:#333333;}#mermaid-svg-m6tMN0QZ2Q09VtlT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-m6tMN0QZ2Q09VtlT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-m6tMN0QZ2Q09VtlT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-m6tMN0QZ2Q09VtlT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-m6tMN0QZ2Q09VtlT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-m6tMN0QZ2Q09VtlT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-m6tMN0QZ2Q09VtlT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-m6tMN0QZ2Q09VtlT .cluster text{fill:#333;}#mermaid-svg-m6tMN0QZ2Q09VtlT .cluster span{color:#333;}#mermaid-svg-m6tMN0QZ2Q09VtlT 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-m6tMN0QZ2Q09VtlT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-m6tMN0QZ2Q09VtlT rect.text{fill:none;stroke-width:0;}#mermaid-svg-m6tMN0QZ2Q09VtlT .icon-shape,#mermaid-svg-m6tMN0QZ2Q09VtlT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-m6tMN0QZ2Q09VtlT .icon-shape p,#mermaid-svg-m6tMN0QZ2Q09VtlT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-m6tMN0QZ2Q09VtlT .icon-shape .label rect,#mermaid-svg-m6tMN0QZ2Q09VtlT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-m6tMN0QZ2Q09VtlT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-m6tMN0QZ2Q09VtlT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-m6tMN0QZ2Q09VtlT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Redis Cluster 核心特性
去中心化
无代理层,客户端直连节点
16384 哈希槽
CRC16(key) % 16384
主从高可用
每个分片一个主 + 多个从
自动分片与迁移
在线扩容/缩容
Gossip 协议
节点间状态传播
- 去中心化:没有中心代理节点,客户端直连任意集群节点,通过重定向找到正确的节点
- 16384 个哈希槽(Hash Slot):数据分片的逻辑单位,每个节点负责一部分槽
- 内置高可用:每个分片自带主从,主库挂了自动选举新主库(不需要额外的 Sentinel)
- 线性扩展:增加节点即可扩展容量和吞吐量
二、哈希槽(Hash Slot):数据分片的核心
2.1 为什么是 16384?
Redis Cluster 没有使用一致性哈希,而是采用了哈希槽 。总共 16384(2^14) 个槽,每个 key 通过 CRC16(key) % 16384 计算落在哪个槽上。
#mermaid-svg-NOfcK2h5h0ef2Rju{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-NOfcK2h5h0ef2Rju .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NOfcK2h5h0ef2Rju .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NOfcK2h5h0ef2Rju .error-icon{fill:#552222;}#mermaid-svg-NOfcK2h5h0ef2Rju .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NOfcK2h5h0ef2Rju .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NOfcK2h5h0ef2Rju .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NOfcK2h5h0ef2Rju .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NOfcK2h5h0ef2Rju .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NOfcK2h5h0ef2Rju .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NOfcK2h5h0ef2Rju .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NOfcK2h5h0ef2Rju .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NOfcK2h5h0ef2Rju .marker.cross{stroke:#333333;}#mermaid-svg-NOfcK2h5h0ef2Rju svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NOfcK2h5h0ef2Rju p{margin:0;}#mermaid-svg-NOfcK2h5h0ef2Rju .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NOfcK2h5h0ef2Rju .cluster-label text{fill:#333;}#mermaid-svg-NOfcK2h5h0ef2Rju .cluster-label span{color:#333;}#mermaid-svg-NOfcK2h5h0ef2Rju .cluster-label span p{background-color:transparent;}#mermaid-svg-NOfcK2h5h0ef2Rju .label text,#mermaid-svg-NOfcK2h5h0ef2Rju span{fill:#333;color:#333;}#mermaid-svg-NOfcK2h5h0ef2Rju .node rect,#mermaid-svg-NOfcK2h5h0ef2Rju .node circle,#mermaid-svg-NOfcK2h5h0ef2Rju .node ellipse,#mermaid-svg-NOfcK2h5h0ef2Rju .node polygon,#mermaid-svg-NOfcK2h5h0ef2Rju .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NOfcK2h5h0ef2Rju .rough-node .label text,#mermaid-svg-NOfcK2h5h0ef2Rju .node .label text,#mermaid-svg-NOfcK2h5h0ef2Rju .image-shape .label,#mermaid-svg-NOfcK2h5h0ef2Rju .icon-shape .label{text-anchor:middle;}#mermaid-svg-NOfcK2h5h0ef2Rju .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NOfcK2h5h0ef2Rju .rough-node .label,#mermaid-svg-NOfcK2h5h0ef2Rju .node .label,#mermaid-svg-NOfcK2h5h0ef2Rju .image-shape .label,#mermaid-svg-NOfcK2h5h0ef2Rju .icon-shape .label{text-align:center;}#mermaid-svg-NOfcK2h5h0ef2Rju .node.clickable{cursor:pointer;}#mermaid-svg-NOfcK2h5h0ef2Rju .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NOfcK2h5h0ef2Rju .arrowheadPath{fill:#333333;}#mermaid-svg-NOfcK2h5h0ef2Rju .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NOfcK2h5h0ef2Rju .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NOfcK2h5h0ef2Rju .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NOfcK2h5h0ef2Rju .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NOfcK2h5h0ef2Rju .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NOfcK2h5h0ef2Rju .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NOfcK2h5h0ef2Rju .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NOfcK2h5h0ef2Rju .cluster text{fill:#333;}#mermaid-svg-NOfcK2h5h0ef2Rju .cluster span{color:#333;}#mermaid-svg-NOfcK2h5h0ef2Rju 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-NOfcK2h5h0ef2Rju .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NOfcK2h5h0ef2Rju rect.text{fill:none;stroke-width:0;}#mermaid-svg-NOfcK2h5h0ef2Rju .icon-shape,#mermaid-svg-NOfcK2h5h0ef2Rju .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NOfcK2h5h0ef2Rju .icon-shape p,#mermaid-svg-NOfcK2h5h0ef2Rju .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NOfcK2h5h0ef2Rju .icon-shape .label rect,#mermaid-svg-NOfcK2h5h0ef2Rju .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NOfcK2h5h0ef2Rju .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NOfcK2h5h0ef2Rju .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NOfcK2h5h0ef2Rju :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 集群节点
16384 个 Hash Slot
Slot 0
Slot 1
...
Slot 5460
Slot 5461
...
Slot 10922
Slot 10923
...
Slot 16383
Node A
Slot 0 ~ 5460
(5461 个)
Node B
Slot 5461 ~ 10922
(5462 个)
Node C
Slot 10923 ~ 16383
(5461 个)
为什么是 16384 而不是 65535? Redis 作者 antirez 给过解释:
| 原因 | 说明 |
|---|---|
| 心跳包大小 | 节点间 Gossip 消息中包含本节点的槽位图(bitmap),16384 个槽 = 2KB,65535 = 8KB。16384 刚好在 TCP 报文不拆包的前提下最大化 |
| 节点数预期 | 集群规模通常在 1000 以内,16384 个槽足够均匀分配 |
| 位图传输效率 | 16384 bit = 2048 字节,压缩后更小 |
2.2 Hash Tag:把多个 key 放到同一个槽
有时候我们需要多个 key 落在同一个节点上(比如事务、Lua 脚本需要操作多个 key)。Hash Tag 语法:
bash
# 只用 {} 内的部分计算哈希
SET user:{1001}:name "张三"
SET user:{1001}:age 28
SET user:{1001}:email "zhangsan@example.com"
# 这三个 key 都只对 "1001" 做 CRC16,保证落在同一个槽
原理 :如果 key 中包含 {},Redis 只对 {} 中间的部分计算 CRC16:
CRC16("user:{1001}:name") → 只用 "1001" 计算
CRC16("user:{1001}:age") → 只用 "1001" 计算 ← 同一槽
CRC16("user:{1001}:email") → 只用 "1001" 计算 ← 同一槽
⚠️ 慎用 Hash Tag:过度使用会导致数据倾斜------大量 key 集中在一个槽,热点节点成为瓶颈。
三、MOVED 与 ASK 重定向:客户端如何找到正确的节点?
3.1 MOVED 重定向 ------ "你找错人了,去那边"
当你向任意一个集群节点发送命令,如果该节点不负责这个 key 对应的槽,它会返回 MOVED 错误:
bash
# 客户端连接到 Node A(负责 0~5460)
redis-cli -c -h nodeA -p 6379 SET key_in_slot_10000 "value"
# Node A 返回:
-MOVED 10000 nodeC:6379
# ↑ ↑
# 槽号 正确的节点地址
Node C(负责槽10923-16383) Node A(负责槽0-5460) 客户端 Node C(负责槽10923-16383) Node A(负责槽0-5460) 客户端 #mermaid-svg-bPWTV6UL2PSQAgRw{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-bPWTV6UL2PSQAgRw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bPWTV6UL2PSQAgRw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bPWTV6UL2PSQAgRw .error-icon{fill:#552222;}#mermaid-svg-bPWTV6UL2PSQAgRw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bPWTV6UL2PSQAgRw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bPWTV6UL2PSQAgRw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bPWTV6UL2PSQAgRw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bPWTV6UL2PSQAgRw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bPWTV6UL2PSQAgRw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bPWTV6UL2PSQAgRw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bPWTV6UL2PSQAgRw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bPWTV6UL2PSQAgRw .marker.cross{stroke:#333333;}#mermaid-svg-bPWTV6UL2PSQAgRw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bPWTV6UL2PSQAgRw p{margin:0;}#mermaid-svg-bPWTV6UL2PSQAgRw .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bPWTV6UL2PSQAgRw text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-bPWTV6UL2PSQAgRw .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-bPWTV6UL2PSQAgRw .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-bPWTV6UL2PSQAgRw .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-bPWTV6UL2PSQAgRw .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-bPWTV6UL2PSQAgRw #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-bPWTV6UL2PSQAgRw .sequenceNumber{fill:white;}#mermaid-svg-bPWTV6UL2PSQAgRw #sequencenumber{fill:#333;}#mermaid-svg-bPWTV6UL2PSQAgRw #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-bPWTV6UL2PSQAgRw .messageText{fill:#333;stroke:none;}#mermaid-svg-bPWTV6UL2PSQAgRw .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bPWTV6UL2PSQAgRw .labelText,#mermaid-svg-bPWTV6UL2PSQAgRw .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-bPWTV6UL2PSQAgRw .loopText,#mermaid-svg-bPWTV6UL2PSQAgRw .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-bPWTV6UL2PSQAgRw .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-bPWTV6UL2PSQAgRw .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-bPWTV6UL2PSQAgRw .noteText,#mermaid-svg-bPWTV6UL2PSQAgRw .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-bPWTV6UL2PSQAgRw .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bPWTV6UL2PSQAgRw .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bPWTV6UL2PSQAgRw .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-bPWTV6UL2PSQAgRw .actorPopupMenu{position:absolute;}#mermaid-svg-bPWTV6UL2PSQAgRw .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-bPWTV6UL2PSQAgRw .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-bPWTV6UL2PSQAgRw .actor-man circle,#mermaid-svg-bPWTV6UL2PSQAgRw line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-bPWTV6UL2PSQAgRw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} SET key_in_slot_10000 "value"CRC16(key) % 16384 = 10000-MOVED 10000 NodeC:6379SET key_in_slot_10000 "value"OK
注意:
- MOVED 意味着永久重定向------这个槽确实不属于当前节点
- 智能客户端(Jedis Cluster、Lettuce)会缓存槽位映射,下次相同的槽直接发到正确节点
redis-cli -c开启了自动重定向模式,客户端自动跟随 MOVED
3.2 ASK 重定向 ------ "这个槽正在搬家,去那边试试"
当槽在迁移过程中(MIGRATING 状态),目标节点没有该 key 时会返回 ASK:
bash
# 槽 10000 正在从 Node A 迁移到 Node C
# 客户端连接 Node A(源节点)
redis-cli -h nodeA -p 6379 GET key_in_slot_10000
# Node A:key 已经迁走了,返回
-ASK 10000 nodeC:6379
Node C(目标)槽 10000: IMPORTING Node A(源)槽 10000: MIGRATING 客户端 Node C(目标)槽 10000: IMPORTING Node A(源)槽 10000: MIGRATING 客户端 #mermaid-svg-Gud4pRn4Ct9PeDJi{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-Gud4pRn4Ct9PeDJi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Gud4pRn4Ct9PeDJi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Gud4pRn4Ct9PeDJi .error-icon{fill:#552222;}#mermaid-svg-Gud4pRn4Ct9PeDJi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Gud4pRn4Ct9PeDJi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Gud4pRn4Ct9PeDJi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Gud4pRn4Ct9PeDJi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Gud4pRn4Ct9PeDJi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Gud4pRn4Ct9PeDJi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Gud4pRn4Ct9PeDJi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Gud4pRn4Ct9PeDJi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Gud4pRn4Ct9PeDJi .marker.cross{stroke:#333333;}#mermaid-svg-Gud4pRn4Ct9PeDJi svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Gud4pRn4Ct9PeDJi p{margin:0;}#mermaid-svg-Gud4pRn4Ct9PeDJi .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Gud4pRn4Ct9PeDJi text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Gud4pRn4Ct9PeDJi .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Gud4pRn4Ct9PeDJi .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Gud4pRn4Ct9PeDJi .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Gud4pRn4Ct9PeDJi .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Gud4pRn4Ct9PeDJi #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Gud4pRn4Ct9PeDJi .sequenceNumber{fill:white;}#mermaid-svg-Gud4pRn4Ct9PeDJi #sequencenumber{fill:#333;}#mermaid-svg-Gud4pRn4Ct9PeDJi #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Gud4pRn4Ct9PeDJi .messageText{fill:#333;stroke:none;}#mermaid-svg-Gud4pRn4Ct9PeDJi .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Gud4pRn4Ct9PeDJi .labelText,#mermaid-svg-Gud4pRn4Ct9PeDJi .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Gud4pRn4Ct9PeDJi .loopText,#mermaid-svg-Gud4pRn4Ct9PeDJi .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Gud4pRn4Ct9PeDJi .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-Gud4pRn4Ct9PeDJi .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Gud4pRn4Ct9PeDJi .noteText,#mermaid-svg-Gud4pRn4Ct9PeDJi .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Gud4pRn4Ct9PeDJi .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Gud4pRn4Ct9PeDJi .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Gud4pRn4Ct9PeDJi .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Gud4pRn4Ct9PeDJi .actorPopupMenu{position:absolute;}#mermaid-svg-Gud4pRn4Ct9PeDJi .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-Gud4pRn4Ct9PeDJi .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Gud4pRn4Ct9PeDJi .actor-man circle,#mermaid-svg-Gud4pRn4Ct9PeDJi line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Gud4pRn4Ct9PeDJi :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} key_x 已经迁到 Node C GET key_x-ASK 10000 NodeC:6379ASKING(告知:我被 ASK 过来的,允许查 IMPORTING 槽)GET key_x"value"
3.3 MOVED vs ASK 对比
| 维度 | MOVED | ASK |
|---|---|---|
| 含义 | 槽永久归属其他节点 | 槽正在迁移,临时的 |
| 触发时机 | 任何时候,槽不属于当前节点 | 槽在 MIGRATING/IMPORTING 状态 |
| 客户端行为 | 更新槽位缓存,下次直接发送 | 不更新缓存,下次仍发原节点(临时性) |
| 特殊前置 | 无 | 必须先发送 ASKING 命令 |
| 迁移完成后 | 新节点正常响应 | ASK 消失,迁移期间的 key 归新节点 |
四、Gossip 协议:去中心化的节点通信
4.1 为什么用 Gossip?
Redis Cluster 是去中心化的,没有 ZooKeeper 那样的协调中心。节点之间需要知道:
- 哪些节点在线/离线
- 每个节点负责哪些槽
- 主从关系是怎样的
- 哪个节点被选为新的主库
Gossip 协议 就是为这种场景量身定做的------每个节点随机挑选几个其他节点,定期交换信息,最终整个集群达到一致状态。
4.2 Gossip 消息类型
Redis Cluster 的节点通信端口 = 服务端口 + 10000(如 6379 → 16379)。
#mermaid-svg-tqocvDac5MJuZsso{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-tqocvDac5MJuZsso .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-tqocvDac5MJuZsso .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-tqocvDac5MJuZsso .error-icon{fill:#552222;}#mermaid-svg-tqocvDac5MJuZsso .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-tqocvDac5MJuZsso .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-tqocvDac5MJuZsso .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-tqocvDac5MJuZsso .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-tqocvDac5MJuZsso .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-tqocvDac5MJuZsso .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-tqocvDac5MJuZsso .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-tqocvDac5MJuZsso .marker{fill:#333333;stroke:#333333;}#mermaid-svg-tqocvDac5MJuZsso .marker.cross{stroke:#333333;}#mermaid-svg-tqocvDac5MJuZsso svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-tqocvDac5MJuZsso p{margin:0;}#mermaid-svg-tqocvDac5MJuZsso .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-tqocvDac5MJuZsso .cluster-label text{fill:#333;}#mermaid-svg-tqocvDac5MJuZsso .cluster-label span{color:#333;}#mermaid-svg-tqocvDac5MJuZsso .cluster-label span p{background-color:transparent;}#mermaid-svg-tqocvDac5MJuZsso .label text,#mermaid-svg-tqocvDac5MJuZsso span{fill:#333;color:#333;}#mermaid-svg-tqocvDac5MJuZsso .node rect,#mermaid-svg-tqocvDac5MJuZsso .node circle,#mermaid-svg-tqocvDac5MJuZsso .node ellipse,#mermaid-svg-tqocvDac5MJuZsso .node polygon,#mermaid-svg-tqocvDac5MJuZsso .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-tqocvDac5MJuZsso .rough-node .label text,#mermaid-svg-tqocvDac5MJuZsso .node .label text,#mermaid-svg-tqocvDac5MJuZsso .image-shape .label,#mermaid-svg-tqocvDac5MJuZsso .icon-shape .label{text-anchor:middle;}#mermaid-svg-tqocvDac5MJuZsso .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-tqocvDac5MJuZsso .rough-node .label,#mermaid-svg-tqocvDac5MJuZsso .node .label,#mermaid-svg-tqocvDac5MJuZsso .image-shape .label,#mermaid-svg-tqocvDac5MJuZsso .icon-shape .label{text-align:center;}#mermaid-svg-tqocvDac5MJuZsso .node.clickable{cursor:pointer;}#mermaid-svg-tqocvDac5MJuZsso .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-tqocvDac5MJuZsso .arrowheadPath{fill:#333333;}#mermaid-svg-tqocvDac5MJuZsso .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-tqocvDac5MJuZsso .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-tqocvDac5MJuZsso .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tqocvDac5MJuZsso .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-tqocvDac5MJuZsso .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tqocvDac5MJuZsso .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-tqocvDac5MJuZsso .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-tqocvDac5MJuZsso .cluster text{fill:#333;}#mermaid-svg-tqocvDac5MJuZsso .cluster span{color:#333;}#mermaid-svg-tqocvDac5MJuZsso 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-tqocvDac5MJuZsso .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-tqocvDac5MJuZsso rect.text{fill:none;stroke-width:0;}#mermaid-svg-tqocvDac5MJuZsso .icon-shape,#mermaid-svg-tqocvDac5MJuZsso .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-tqocvDac5MJuZsso .icon-shape p,#mermaid-svg-tqocvDac5MJuZsso .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-tqocvDac5MJuZsso .icon-shape .label rect,#mermaid-svg-tqocvDac5MJuZsso .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-tqocvDac5MJuZsso .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-tqocvDac5MJuZsso .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-tqocvDac5MJuZsso :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Gossip 消息类型
MEET
邀请新节点加入集群
PING
心跳 + 交换状态
PONG
回复 PING / MEET
FAIL
广播某节点已故障
PUBLISH
Pub/Sub 消息传播
UPDATE
通知节点更新配置
4.3 PING/PONG 的详细工作流程
渲染错误: Mermaid 渲染失败: Parse error on line 18: ... Note over A,B,C: 通过反复的 PING/PONG ----------------------^ Expecting 'TXT', got ','
PING 的频率:
- 每秒钟随机选 5 个节点发送 PING
- 如果某个节点超过
cluster-node-timeout / 2(默认 7.5 秒)没收到 PONG,立即发送 PING - 每个 PING 包的 Gossip 体包含 3 个随机节点的状态信息
4.4 Gossip 的优缺点
| 优点 | 缺点 |
|---|---|
| 去中心化,无单点故障 | 最终一致性,有收敛延迟 |
| 扩展性好,节点增加不影响架构 | 消息冗余(同一信息可能被多次传递) |
| 实现简单,维护成本低 | 大规模集群下消息量 O(N × 节点数) |
| 对网络分区有自愈能力 | 故障检测依赖于超时判断,不是即时的 |
五、Cluster 的故障检测与自动故障转移
5.1 故障检测:PFAIL → FAIL
Redis Cluster 的故障检测也是两步判断,和 Sentinel 的 SDOWN/ODOWN 如出一辙:
#mermaid-svg-sN36WpU5DUJaBxUI{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-sN36WpU5DUJaBxUI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sN36WpU5DUJaBxUI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sN36WpU5DUJaBxUI .error-icon{fill:#552222;}#mermaid-svg-sN36WpU5DUJaBxUI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sN36WpU5DUJaBxUI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sN36WpU5DUJaBxUI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sN36WpU5DUJaBxUI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sN36WpU5DUJaBxUI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sN36WpU5DUJaBxUI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sN36WpU5DUJaBxUI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sN36WpU5DUJaBxUI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sN36WpU5DUJaBxUI .marker.cross{stroke:#333333;}#mermaid-svg-sN36WpU5DUJaBxUI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sN36WpU5DUJaBxUI p{margin:0;}#mermaid-svg-sN36WpU5DUJaBxUI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sN36WpU5DUJaBxUI .cluster-label text{fill:#333;}#mermaid-svg-sN36WpU5DUJaBxUI .cluster-label span{color:#333;}#mermaid-svg-sN36WpU5DUJaBxUI .cluster-label span p{background-color:transparent;}#mermaid-svg-sN36WpU5DUJaBxUI .label text,#mermaid-svg-sN36WpU5DUJaBxUI span{fill:#333;color:#333;}#mermaid-svg-sN36WpU5DUJaBxUI .node rect,#mermaid-svg-sN36WpU5DUJaBxUI .node circle,#mermaid-svg-sN36WpU5DUJaBxUI .node ellipse,#mermaid-svg-sN36WpU5DUJaBxUI .node polygon,#mermaid-svg-sN36WpU5DUJaBxUI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sN36WpU5DUJaBxUI .rough-node .label text,#mermaid-svg-sN36WpU5DUJaBxUI .node .label text,#mermaid-svg-sN36WpU5DUJaBxUI .image-shape .label,#mermaid-svg-sN36WpU5DUJaBxUI .icon-shape .label{text-anchor:middle;}#mermaid-svg-sN36WpU5DUJaBxUI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sN36WpU5DUJaBxUI .rough-node .label,#mermaid-svg-sN36WpU5DUJaBxUI .node .label,#mermaid-svg-sN36WpU5DUJaBxUI .image-shape .label,#mermaid-svg-sN36WpU5DUJaBxUI .icon-shape .label{text-align:center;}#mermaid-svg-sN36WpU5DUJaBxUI .node.clickable{cursor:pointer;}#mermaid-svg-sN36WpU5DUJaBxUI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sN36WpU5DUJaBxUI .arrowheadPath{fill:#333333;}#mermaid-svg-sN36WpU5DUJaBxUI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sN36WpU5DUJaBxUI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sN36WpU5DUJaBxUI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sN36WpU5DUJaBxUI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sN36WpU5DUJaBxUI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sN36WpU5DUJaBxUI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sN36WpU5DUJaBxUI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sN36WpU5DUJaBxUI .cluster text{fill:#333;}#mermaid-svg-sN36WpU5DUJaBxUI .cluster span{color:#333;}#mermaid-svg-sN36WpU5DUJaBxUI 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-sN36WpU5DUJaBxUI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sN36WpU5DUJaBxUI rect.text{fill:none;stroke-width:0;}#mermaid-svg-sN36WpU5DUJaBxUI .icon-shape,#mermaid-svg-sN36WpU5DUJaBxUI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sN36WpU5DUJaBxUI .icon-shape p,#mermaid-svg-sN36WpU5DUJaBxUI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sN36WpU5DUJaBxUI .icon-shape .label rect,#mermaid-svg-sN36WpU5DUJaBxUI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sN36WpU5DUJaBxUI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sN36WpU5DUJaBxUI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sN36WpU5DUJaBxUI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
≥ 半数主节点
不足半数
Node A 向 Node B 发 PING
超时?
cluster-node-timeout
Node A 标记 Node B 为 PFAIL
(Possible Failure,疑似下线)
正常
Node A 通过 Gossip
向其他节点传播 PFAIL 信息
多少节点认为 B 是 PFAIL?
Node A 将 B 标记为 FAIL
并广播 FAIL 消息
等待更多反馈
触发故障转移
PFAIL(疑似下线):单个节点的判断,相当于 Sentinel 的 SDOWN。
FAIL(确认下线):多个主节点达成共识后的判断,相当于 Sentinel 的 ODOWN。
5.2 自动故障转移(Failover)
Cluster 的故障转移流程与 Sentinel 类似,但不需要独立的 Sentinel 进程------集群中的其他主节点充当了 Sentinel 的角色。
集群 故障主库的从库 其他主节点 集群 故障主库的从库 其他主节点 #mermaid-svg-n9RMxM4SOIEeadc3{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-n9RMxM4SOIEeadc3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-n9RMxM4SOIEeadc3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-n9RMxM4SOIEeadc3 .error-icon{fill:#552222;}#mermaid-svg-n9RMxM4SOIEeadc3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-n9RMxM4SOIEeadc3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-n9RMxM4SOIEeadc3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-n9RMxM4SOIEeadc3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-n9RMxM4SOIEeadc3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-n9RMxM4SOIEeadc3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-n9RMxM4SOIEeadc3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-n9RMxM4SOIEeadc3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-n9RMxM4SOIEeadc3 .marker.cross{stroke:#333333;}#mermaid-svg-n9RMxM4SOIEeadc3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-n9RMxM4SOIEeadc3 p{margin:0;}#mermaid-svg-n9RMxM4SOIEeadc3 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-n9RMxM4SOIEeadc3 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-n9RMxM4SOIEeadc3 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-n9RMxM4SOIEeadc3 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-n9RMxM4SOIEeadc3 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-n9RMxM4SOIEeadc3 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-n9RMxM4SOIEeadc3 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-n9RMxM4SOIEeadc3 .sequenceNumber{fill:white;}#mermaid-svg-n9RMxM4SOIEeadc3 #sequencenumber{fill:#333;}#mermaid-svg-n9RMxM4SOIEeadc3 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-n9RMxM4SOIEeadc3 .messageText{fill:#333;stroke:none;}#mermaid-svg-n9RMxM4SOIEeadc3 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-n9RMxM4SOIEeadc3 .labelText,#mermaid-svg-n9RMxM4SOIEeadc3 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-n9RMxM4SOIEeadc3 .loopText,#mermaid-svg-n9RMxM4SOIEeadc3 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-n9RMxM4SOIEeadc3 .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-n9RMxM4SOIEeadc3 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-n9RMxM4SOIEeadc3 .noteText,#mermaid-svg-n9RMxM4SOIEeadc3 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-n9RMxM4SOIEeadc3 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-n9RMxM4SOIEeadc3 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-n9RMxM4SOIEeadc3 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-n9RMxM4SOIEeadc3 .actorPopupMenu{position:absolute;}#mermaid-svg-n9RMxM4SOIEeadc3 .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-n9RMxM4SOIEeadc3 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-n9RMxM4SOIEeadc3 .actor-man circle,#mermaid-svg-n9RMxM4SOIEeadc3 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-n9RMxM4SOIEeadc3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 某主库被标记为 FAIL 发现自己主库 FAIL等待 cluster-node-timeout × 2发起选举(请求投票)在当前纪元中是否已投票?从库的数据是否足够新(offset 差距是否在可接受范围)同意投票获得 ≥ N/2+1 票升级为主库广播新配置接管原主库的槽通知其他从库指向新主库
选举规则:
- 故障主库的所有从库都有资格参选
- 从库发现主库 FAIL 后,等待
500ms + 随机(0~500ms) + rank × 1000ms后发起选举 - rank 越低越优先------rank 是根据从库的数据复制偏移量计算的,数据越新 rank 越小
- 获得过半数主节点投票的从库胜出
- 如果没人胜出,下一个纪元继续
5.3 集群与 Sentinel 故障转移对比
| 维度 | Sentinel | Cluster |
|---|---|---|
| 监控者 | 独立 Sentinel 进程 | 集群中其他主节点 |
| 额外部署 | 需要 Sentinel 集群(3台+) | 不需要,内建能力 |
| 故障判定 | SDOWN → ODOWN(quorum) | PFAIL → FAIL(半数主节点) |
| 选主逻辑 | replica-priority > offset > runid | offset 越新 rank 越小 |
| 注册中心 | Sentinel 充当 | 任何集群节点都能返回槽位信息 |
六、集群扩容与缩容:在线迁移实战
6.1 扩容流程
假设当前集群有 3 个主节点,需要扩容到 4 个:
#mermaid-svg-PKL4wb3ir7ZpmajA{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-PKL4wb3ir7ZpmajA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PKL4wb3ir7ZpmajA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PKL4wb3ir7ZpmajA .error-icon{fill:#552222;}#mermaid-svg-PKL4wb3ir7ZpmajA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PKL4wb3ir7ZpmajA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PKL4wb3ir7ZpmajA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PKL4wb3ir7ZpmajA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PKL4wb3ir7ZpmajA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PKL4wb3ir7ZpmajA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PKL4wb3ir7ZpmajA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PKL4wb3ir7ZpmajA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PKL4wb3ir7ZpmajA .marker.cross{stroke:#333333;}#mermaid-svg-PKL4wb3ir7ZpmajA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PKL4wb3ir7ZpmajA p{margin:0;}#mermaid-svg-PKL4wb3ir7ZpmajA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PKL4wb3ir7ZpmajA .cluster-label text{fill:#333;}#mermaid-svg-PKL4wb3ir7ZpmajA .cluster-label span{color:#333;}#mermaid-svg-PKL4wb3ir7ZpmajA .cluster-label span p{background-color:transparent;}#mermaid-svg-PKL4wb3ir7ZpmajA .label text,#mermaid-svg-PKL4wb3ir7ZpmajA span{fill:#333;color:#333;}#mermaid-svg-PKL4wb3ir7ZpmajA .node rect,#mermaid-svg-PKL4wb3ir7ZpmajA .node circle,#mermaid-svg-PKL4wb3ir7ZpmajA .node ellipse,#mermaid-svg-PKL4wb3ir7ZpmajA .node polygon,#mermaid-svg-PKL4wb3ir7ZpmajA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PKL4wb3ir7ZpmajA .rough-node .label text,#mermaid-svg-PKL4wb3ir7ZpmajA .node .label text,#mermaid-svg-PKL4wb3ir7ZpmajA .image-shape .label,#mermaid-svg-PKL4wb3ir7ZpmajA .icon-shape .label{text-anchor:middle;}#mermaid-svg-PKL4wb3ir7ZpmajA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PKL4wb3ir7ZpmajA .rough-node .label,#mermaid-svg-PKL4wb3ir7ZpmajA .node .label,#mermaid-svg-PKL4wb3ir7ZpmajA .image-shape .label,#mermaid-svg-PKL4wb3ir7ZpmajA .icon-shape .label{text-align:center;}#mermaid-svg-PKL4wb3ir7ZpmajA .node.clickable{cursor:pointer;}#mermaid-svg-PKL4wb3ir7ZpmajA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PKL4wb3ir7ZpmajA .arrowheadPath{fill:#333333;}#mermaid-svg-PKL4wb3ir7ZpmajA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PKL4wb3ir7ZpmajA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PKL4wb3ir7ZpmajA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PKL4wb3ir7ZpmajA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PKL4wb3ir7ZpmajA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PKL4wb3ir7ZpmajA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PKL4wb3ir7ZpmajA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PKL4wb3ir7ZpmajA .cluster text{fill:#333;}#mermaid-svg-PKL4wb3ir7ZpmajA .cluster span{color:#333;}#mermaid-svg-PKL4wb3ir7ZpmajA 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-PKL4wb3ir7ZpmajA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PKL4wb3ir7ZpmajA rect.text{fill:none;stroke-width:0;}#mermaid-svg-PKL4wb3ir7ZpmajA .icon-shape,#mermaid-svg-PKL4wb3ir7ZpmajA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PKL4wb3ir7ZpmajA .icon-shape p,#mermaid-svg-PKL4wb3ir7ZpmajA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PKL4wb3ir7ZpmajA .icon-shape .label rect,#mermaid-svg-PKL4wb3ir7ZpmajA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PKL4wb3ir7ZpmajA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PKL4wb3ir7ZpmajA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PKL4wb3ir7ZpmajA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Step 3: 为新节点添加从库(可选)
redis-cli --cluster add-node
从库IP:6380 主库IP:6379 --cluster-slave
Step 2: 重新分配槽
redis-cli --cluster reshard
从每个旧节点迁移部分槽到新节点
3 个节点 × 16384 槽 ÷ 4 = 每节点 4096 槽
每个旧节点迁出 ~1365 槽
Step 1: 新节点加入集群
redis-cli --cluster add-node
新节点IP:6379 集群中任一节点IP:6379
实际操作命令:
bash
# 1. 添加新主节点到集群
redis-cli --cluster add-node 192.168.1.104:6379 192.168.1.101:6379
# ↑ 新节点 ↑ 集群中任意节点
# 2. 重新分片(会进入交互模式)
redis-cli --cluster reshard 192.168.1.101:6379
# 交互提示:
# How many slots do you want to move? 4096
# What is the receiving node ID? <新节点的 node-id>
# Source node #1: all (从所有已有节点平均迁出)
# Source node #2: done
# 3. 检查迁移进度
redis-cli --cluster check 192.168.1.101:6379
# 4. 添加从库
redis-cli --cluster add-node 192.168.1.104:6380 192.168.1.104:6379 --cluster-slave
6.2 槽迁移的底层过程
单个槽的迁移过程(同步完成):
源节点状态变化:STABLE → MIGRATING(准备迁出)
目标节点状态变化:STABLE → IMPORTING(准备迁入)
迁移每个 key 的步骤:
1. 源节点 DUMP key → 序列化
2. 目标节点 RESTORE key → 反序列化
3. 源节点 DEL key(如果 RESTORE 成功)
4. DELETE 完成后,槽位信息推送到集群
迁移期间,对迁移中的 key 的访问:
| 请求位置 | key 状态 | 响应 |
|---|---|---|
| 源节点 | key 还在 | 正常返回 |
| 源节点 | key 已迁走 | -ASK 重定向到目标节点 |
| 目标节点 | key 已迁入 | 需要 ASKING 前置 → 正常返回 |
| 目标节点 | key 还没来 | -MOVED 到源节点 |
6.3 缩容流程
bash
# 1. 迁移要下线节点的所有槽到其他节点
redis-cli --cluster reshard 192.168.1.101:6379
# Source node: 指定要下线节点的 node-id
# Destination node: 指定接收节点
# How many slots: 这个节点的槽总数
# 2. 确认槽已清空后,下线节点
redis-cli --cluster del-node 192.168.1.101:6379 <要下线节点的node-id>
# 3. 如果节点有从库,先删从库再删主库
七、生产环境部署与配置
7.1 最小集群拓扑
生产环境的最小推荐配置:3 主 3 从(共 6 个 Redis 实例)
┌────────────────────────────────────────────────┐
│ Redis Cluster │
├──────────┬──────────┬──────────┬───────────────┤
│ Master-1 │ Master-2 │ Master-3 │ │
│ :6379 │ :6380 │ :6381 │ │
│ 槽0~5460 │ 槽5461~ │ 槽10923~ │ │
│ │ 10922 │ 16383 │ │
├──────────┼──────────┼──────────┤ │
│ Slave-1 │ Slave-2 │ Slave-3 │ │
│ :6382 │ :6383 │ :6384 │ │
└──────────┴──────────┴──────────┴───────────────┘
为什么是 3 主 3 从? 如果只有 2 个主节点,一个主节点挂了,剩余的节点无法形成过半数(需要 ≥ N/2+1 = 2 票,但只有一个能投票)。3 个主节点才能容忍 1 个主节点故障。
7.2 完整 Cluster 配置
bash
# redis.conf ------ 每个集群节点通用配置
# ============ 基本配置 ============
port 6379
bind 0.0.0.0
cluster-enabled yes # 开启集群模式
cluster-config-file nodes-6379.conf # 集群状态持久化文件(自动生成)
cluster-node-timeout 15000 # 节点超时(毫秒),默认 15 秒
# ============ 持久化(强烈建议开启)============
save 900 1
save 300 10
save 60 10000
appendonly yes
appendfsync everysec
# ============ 主从配置 ============
# 不需要 replicaof ------ 集群模式下由集群自动管理主从关系
# 从库必须设置,否则主库故障后从库无法升主
masterauth "your_password"
# ============ 集群相关优化 ============
# 控制是否在从库故障时自动迁移副本
cluster-migration-barrier 1
# 含义:如果主库拥有 ≥ 1 个从库,当某个主库失去所有从库时,
# 从从库多余的主库上迁移一个从库过去(保持每个主库都有从库)
# 集群消息是否需要完整数据校验
cluster-require-full-coverage yes
# yes:任一槽位不可用时集群停止服务(推荐,数据安全)
# no:部分槽不可用仍继续服务(高可用优先)
# 副本有效性因子(replica-validity-factor)
cluster-replica-validity-factor 10
# 从库与主库断连超过 cluster-node-timeout × 10 后
# 失去故障转移资格,避免数据落后的从库升主
7.3 创建集群
bash
# Redis 5.0+ 推荐方式
redis-cli --cluster create \
192.168.1.101:6379 \
192.168.1.102:6379 \
192.168.1.103:6379 \
192.168.1.101:6380 \
192.168.1.102:6380 \
192.168.1.103:6380 \
--cluster-replicas 1
# ↑ 每个主库配 1 个从库(自动配对)
# 验证集群状态
redis-cli --cluster check 192.168.1.101:6379
# 查看槽分配
redis-cli -c -h 192.168.1.101 -p 6379 CLUSTER SLOTS
redis-cli -c -h 192.168.1.101 -p 6379 CLUSTER NODES
7.4 集群状态检查常用命令
bash
# 查看集群节点信息
CLUSTER NODES
# 输出解读:
# <node-id> <ip:port@cport> <flags> <master-id> <ping-sent> <pong-recv> <epoch> <link-state> <slot-range>
# flags: myself, master, slave, fail?, fail, handshake, noaddr, noflags
# 查看槽位分配
CLUSTER SLOTS
# 查看本节点的集群信息
CLUSTER INFO
# cluster_state:ok ← 集群状态(必须为 ok)
# cluster_slots_assigned:16384
# cluster_slots_ok:16384 ← 可用槽数
# cluster_size:3 ← 主节点数
# 查看 key 属于哪个槽
CLUSTER KEYSLOT mykey # 返回槽号
# 查看槽中有多少个 key
CLUSTER COUNTKEYSINSLOT 1000
# 修复集群(槽位不一致时)
redis-cli --cluster fix 192.168.1.101:6379
八、常见问题与避坑指南
8.1 坑 1:多 key 操作报错
bash
# ❌ 错误------多个 key 不在同一个槽
MGET key1 key2 key3
# (error) CROSSSLOT Keys in request don't hash to the same slot
# ✅ 方案一:使用 Hash Tag
MGET {user:1001}:name {user:1001}:age {user:1001}:email
# ✅ 方案二:业务层拆分,逐个获取后聚合
8.2 坑 2:事务、Lua 脚本限制
Cluster 模式下,MULTI/EXEC 事务和 Lua 脚本中的所有 key 必须在同一个槽。
bash
# ❌ 错误
EVAL "return redis.call('GET', KEYS[1]) .. redis.call('GET', KEYS[2])" 2 key1 key2
# ✅ 使用 Hash Tag
EVAL "..." 2 {group}:key1 {group}:key2
8.3 坑 3:槽位全部不可用时集群停服
bash
# 排查
CLUSTER INFO | grep cluster_state
# cluster_state:fail ← 有槽位不可用
# 常见原因
# - 某个主节点挂了且没有从库可以接替
# - 正在进行槽迁移但过程出错
# - 网络分区导致节点孤立
# 临时恢复(如果确认数据可接受)
cluster-require-full-coverage no # 设置为 no 后重启
8.4 坑 4:客户端不感知 MOVED 重定向
bash
# 如果客户端没有开启集群模式(如直接连单节点用 redis-cli 不加 -c)
SET key "value"
# (error) MOVED 12345 192.168.1.102:6379
# 解决:
# 1. redis-cli 加 -c 参数
redis-cli -c -h 192.168.1.101 -p 6379
# 2. 代码中使用集群客户端
# Jedis: JedisCluster
# Lettuce: RedisClusterClient
# Redisson: RedissonClient (with ClusterServersConfig)
8.5 坑 5:数据倾斜
bash
# 查看各节点的槽数和 key 数量
redis-cli --cluster check 192.168.1.101:6379 | grep "slots"
# 如果发现某节点 key 数量远大于其他节点:
# 1. 检查是否有大量使用了同一个 Hash Tag 的 key
# 2. 手动迁移部分槽(reshard)
# 3. 考虑使用 Redis 7.0 的 rebalance 功能
redis-cli --cluster rebalance 192.168.1.101:6379
九、总结
本文核心知识体系:
#mermaid-svg-kW8z0AqyUxuz65Md{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-kW8z0AqyUxuz65Md .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kW8z0AqyUxuz65Md .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kW8z0AqyUxuz65Md .error-icon{fill:#552222;}#mermaid-svg-kW8z0AqyUxuz65Md .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kW8z0AqyUxuz65Md .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kW8z0AqyUxuz65Md .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kW8z0AqyUxuz65Md .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kW8z0AqyUxuz65Md .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kW8z0AqyUxuz65Md .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kW8z0AqyUxuz65Md .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kW8z0AqyUxuz65Md .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kW8z0AqyUxuz65Md .marker.cross{stroke:#333333;}#mermaid-svg-kW8z0AqyUxuz65Md svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kW8z0AqyUxuz65Md p{margin:0;}#mermaid-svg-kW8z0AqyUxuz65Md .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kW8z0AqyUxuz65Md .cluster-label text{fill:#333;}#mermaid-svg-kW8z0AqyUxuz65Md .cluster-label span{color:#333;}#mermaid-svg-kW8z0AqyUxuz65Md .cluster-label span p{background-color:transparent;}#mermaid-svg-kW8z0AqyUxuz65Md .label text,#mermaid-svg-kW8z0AqyUxuz65Md span{fill:#333;color:#333;}#mermaid-svg-kW8z0AqyUxuz65Md .node rect,#mermaid-svg-kW8z0AqyUxuz65Md .node circle,#mermaid-svg-kW8z0AqyUxuz65Md .node ellipse,#mermaid-svg-kW8z0AqyUxuz65Md .node polygon,#mermaid-svg-kW8z0AqyUxuz65Md .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kW8z0AqyUxuz65Md .rough-node .label text,#mermaid-svg-kW8z0AqyUxuz65Md .node .label text,#mermaid-svg-kW8z0AqyUxuz65Md .image-shape .label,#mermaid-svg-kW8z0AqyUxuz65Md .icon-shape .label{text-anchor:middle;}#mermaid-svg-kW8z0AqyUxuz65Md .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kW8z0AqyUxuz65Md .rough-node .label,#mermaid-svg-kW8z0AqyUxuz65Md .node .label,#mermaid-svg-kW8z0AqyUxuz65Md .image-shape .label,#mermaid-svg-kW8z0AqyUxuz65Md .icon-shape .label{text-align:center;}#mermaid-svg-kW8z0AqyUxuz65Md .node.clickable{cursor:pointer;}#mermaid-svg-kW8z0AqyUxuz65Md .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kW8z0AqyUxuz65Md .arrowheadPath{fill:#333333;}#mermaid-svg-kW8z0AqyUxuz65Md .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kW8z0AqyUxuz65Md .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kW8z0AqyUxuz65Md .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kW8z0AqyUxuz65Md .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kW8z0AqyUxuz65Md .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kW8z0AqyUxuz65Md .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kW8z0AqyUxuz65Md .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kW8z0AqyUxuz65Md .cluster text{fill:#333;}#mermaid-svg-kW8z0AqyUxuz65Md .cluster span{color:#333;}#mermaid-svg-kW8z0AqyUxuz65Md 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-kW8z0AqyUxuz65Md .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kW8z0AqyUxuz65Md rect.text{fill:none;stroke-width:0;}#mermaid-svg-kW8z0AqyUxuz65Md .icon-shape,#mermaid-svg-kW8z0AqyUxuz65Md .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kW8z0AqyUxuz65Md .icon-shape p,#mermaid-svg-kW8z0AqyUxuz65Md .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kW8z0AqyUxuz65Md .icon-shape .label rect,#mermaid-svg-kW8z0AqyUxuz65Md .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kW8z0AqyUxuz65Md .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kW8z0AqyUxuz65Md .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kW8z0AqyUxuz65Md :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Redis Cluster
数据分片
节点通信
高可用
运维
16384 哈希槽
CRC16(key) % 16384
Hash Tag 分组
Gossip 协议
PING/PONG/FAIL/MEET
最终一致性
PFAIL → FAIL
过半投票选主
自动故障转移
add-node / reshard / del-node
MOVED vs ASK 重定向
3 主 3 从最小集群
三个必须理清的概念:
| 概念 | 关键词 | 本质 |
|---|---|---|
| MOVED | 永久重定向 | 槽不属于我,更新本地缓存 |
| ASK | 临时重定向 | 槽在迁移中,不更新缓存 |
| Gossip | 最终一致性 | 随机交换 → 逐步收敛 |
如有疑问或指正,欢迎在评论区交流。