Redis集群模式架构详解
前言
Redis 本质上是个单机服务,一旦崩了,服务就不可用。加几个从节点做备份可以实现高可用,但所有节点存的都是全量数据,无法突破单机内存瓶颈。如果想存储更多数据,该怎么办?这就需要 Redis 集群模式了。
🏠个人主页:你的主页
文章目录
一、为什么需要集群模式
1.1 单机 Redis 的瓶颈
Redis 将数据存储在内存中,单机服务器的内存总有上限。假设一台服务器内存 64GB,Redis 实际可用约 50GB,当数据量超过这个限制就无法继续存储了。
1.2 主从复制的局限
主从复制可以实现高可用:
┌─────────────┐
│ Master │ ← 写入
│ (全量数据) │
└──────┬──────┘
│ 同步
┌───────┴───────┐
↓ ↓
┌─────────────┐ ┌─────────────┐
│ Slave-1 │ │ Slave-2 │ ← 读取
│ (全量数据) │ │ (全量数据) │
└─────────────┘ └─────────────┘
问题:从节点存的是和主节点一样的全量数据,加再多从节点也无法突破单机内存瓶颈。
打个比方:你有一本 500 页的书,复印了 10 份分给 10 个人。虽然有 10 个人可以同时读这本书(读性能提升),但每个人手里还是完整的 500 页,书的内容并没有变多。
1.3 集群模式的思路
既然单机内存有限,那就把数据切分成多份,放到多个 Redis 节点上:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Node-A │ │ Node-B │ │ Node-C │
│ 数据分片1 │ │ 数据分片2 │ │ 数据分片3 │
└─────────────┘ └─────────────┘ └─────────────┘
打个比方:把 500 页的书拆成 3 本,A 拿 1-170 页,B 拿 171-340 页,C 拿 341-500 页。这样每个人只需要保管一部分内容,总容量就扩大了 3 倍。
二、数据分片方案演进
2.1 方案一:取模分片
最直观的想法:对 Key 进行哈希,然后对节点数取模。
节点编号 = hash(key) % 节点总数
举个例子:假设有 3 个 Redis 节点
hash("order:1001") % 3 = 1 → 存到 Node-1
hash("order:1002") % 3 = 2 → 存到 Node-2
hash("order:1003") % 3 = 0 → 存到 Node-0
致命问题:扩容时数据大规模迁移
假设现在要从 3 个节点扩容到 4 个节点:
# 扩容前
hash("order:1001") % 3 = 1 → Node-1
hash("order:1002") % 3 = 2 → Node-2
hash("order:1003") % 3 = 0 → Node-0
# 扩容后(节点数变成4)
hash("order:1001") % 4 = 2 → Node-2 ← 需要迁移!
hash("order:1002") % 4 = 3 → Node-3 ← 需要迁移!
hash("order:1003") % 4 = 1 → Node-1 ← 需要迁移!
几乎所有数据都需要重新分配,迁移成本极高。
2.2 方案二:一致性哈希
一致性哈希将整个哈希值空间组织成一个虚拟的圆环:
0
│
Node-C ──┼── Node-A
╱│╲
╱ │ ╲
╱ │ ╲
╱ │ ╲
╱ │ ╲
╱ │ ╲
Node-B ──┴──────
数据的 Key 经过哈希后落在环上某个位置,然后顺时针找到第一个节点,就是它应该存储的位置。
优点:扩容时只影响相邻节点的数据
缺点:
- 节点少时数据分布不均匀(数据倾斜)
- 需要引入虚拟节点来解决,增加了复杂度
2.3 方案三:哈希槽(Redis Cluster 采用)
Redis Cluster 没有使用一致性哈希,而是引入了**哈希槽(Hash Slot)**的概念。
核心思想:在 Key 和 Node 之间加一层固定长度的中间层。
Key → 哈希槽(固定 16384 个) → Node
为什么这样设计?
没有什么是加一层中间层不能解决的。既然担心节点数变化导致分片结果改变,那就让公式里用于计算的值固定下来。
槽位编号 = CRC16(key) % 16384
不管节点数怎么变,16384 这个数字永远不变,所以同一个 Key 计算出的槽位编号也永远不变。
三、哈希槽机制详解
3.1 哈希槽分配
Redis Cluster 固定有 16384 个哈希槽(编号 0-16383),这些槽位会分配给集群中的各个主节点。
举个例子:3 个主节点的集群
Node-A: 负责槽位 0 ~ 5460 (5461 个槽)
Node-B: 负责槽位 5461 ~ 10922 (5462 个槽)
Node-C: 负责槽位 10923 ~ 16383 (5461 个槽)
3.2 Key 到槽位的映射
当客户端要操作一个 Key 时:
1. 计算 Key 的 CRC16 值
2. 对 16384 取模,得到槽位编号
3. 找到负责这个槽位的节点
4. 向该节点发送命令
举个例子:
Key = "product:10086"
CRC16("product:10086") = 28456
槽位编号 = 28456 % 16384 = 12072
12072 在 10923~16383 范围内 → 由 Node-C 负责
3.3 为什么是 16384 个槽
这是一个权衡的结果:
槽位太少:
- 数据分布不够均匀
- 扩容时迁移粒度太粗
槽位太多:
- 每个节点需要维护的槽位信息太多
- 节点间心跳包携带的数据量太大
16384 = 2^14,是一个比较合适的数字:
- 心跳包中槽位信息只需要 2KB(16384 / 8 = 2048 字节)
- 即使集群有 1000 个节点,每个节点平均也能分到 16 个槽
3.4 Hash Tag:控制 Key 的槽位分配
有时候我们希望某些相关的 Key 落在同一个槽位(同一个节点),可以使用 Hash Tag。
语法 :Key 中用 {} 包裹的部分会被用来计算槽位
# 这三个 Key 会落在同一个槽位
{user:1001}:name
{user:1001}:age
{user:1001}:email
# 因为它们的 Hash Tag 都是 "user:1001"
槽位 = CRC16("user:1001") % 16384
使用场景:
- 需要对多个 Key 执行事务操作
- 需要使用 Lua 脚本操作多个 Key
- 需要使用 MGET/MSET 批量操作
四、集群节点通信机制
4.1 Gossip 协议
Redis Cluster 节点之间使用 Gossip 协议进行通信,这是一种去中心化的协议。
特点:
-
没有中心节点,每个节点地位平等
-
节点之间互相交换信息,最终达到一致
-
类似"八卦传播",信息会逐渐扩散到所有节点
Node-A 发现 Node-D 挂了
↓ 告诉 Node-B
Node-B 知道了
↓ 告诉 Node-C
Node-C 知道了
↓ 最终所有节点都知道 Node-D 挂了
4.2 节点间通信内容
每个节点会维护以下信息:
| 信息类型 | 说明 |
|---|---|
| 节点信息 | 集群中所有节点的 IP、端口、状态 |
| 槽位分配 | 每个槽位由哪个节点负责 |
| 故障信息 | 哪些节点被标记为故障 |
4.3 PING/PONG 心跳
节点之间通过 PING/PONG 消息保持通信:
Node-A → PING → Node-B
Node-B → PONG → Node-A
心跳消息中携带:
- 发送节点的信息
- 发送节点知道的部分其他节点信息(随机选择)
- 槽位分配信息
通过不断交换信息,所有节点最终会达成一致的集群视图。
五、客户端请求路由
5.1 客户端如何知道数据在哪
客户端首次连接集群时,会通过 CLUSTER SLOTS 命令获取槽位分配信息:
bash
127.0.0.1:7000> CLUSTER SLOTS
1) 1) (integer) 0
2) (integer) 5460
3) 1) "192.168.1.101"
2) (integer) 7000
2) 1) (integer) 5461
2) (integer) 10922
3) 1) "192.168.1.102"
2) (integer) 7001
3) 1) (integer) 10923
2) (integer) 16383
3) 1) "192.168.1.103"
2) (integer) 7002
客户端会在本地缓存这份槽位映射表,后续请求直接根据 Key 计算槽位,找到对应节点。
5.2 MOVED 重定向
如果客户端的槽位信息过期了,访问了错误的节点,会收到 MOVED 错误:
bash
# 客户端访问 Node-A,但 Key 实际在 Node-C
127.0.0.1:7000> GET product:10086
(error) MOVED 12072 192.168.1.103:7002
MOVED 的含义:
12072:这个 Key 对应的槽位编号192.168.1.103:7002:负责这个槽位的节点地址
客户端收到 MOVED 后会:
- 更新本地的槽位映射表
- 重新向正确的节点发送请求
打个比方:你去政务大厅办事,走错了窗口,工作人员告诉你"这个业务请去 3 号窗口",你就去 3 号窗口,下次再办同样的事就直接去 3 号窗口了。
5.3 ASK 重定向
ASK 是另一种重定向,发生在数据迁移过程中。
bash
127.0.0.1:7000> GET product:10086
(error) ASK 12072 192.168.1.103:7002
ASK 与 MOVED 的区别:
| 类型 | 含义 | 客户端行为 |
|---|---|---|
| MOVED | 槽位已经永久迁移到新节点 | 更新本地缓存,后续请求直接访问新节点 |
| ASK | 槽位正在迁移中,这个 Key 可能在新节点 | 不更新缓存,只是这一次去新节点查询 |
ASK 的处理流程:
1. 客户端访问 Node-A,查询 Key
2. Node-A 发现这个槽位正在迁移,且本地没有这个 Key
3. Node-A 返回 ASK 重定向
4. 客户端向 Node-C 发送 ASKING 命令
5. 客户端向 Node-C 发送 GET 命令
6. Node-C 返回数据(如果有的话)
六、集群扩缩容与数据迁移
6.1 扩容流程
假设原来有 3 个节点,现在要加入第 4 个节点 Node-D。
步骤一:将新节点加入集群
bash
redis-cli --cluster add-node 192.168.1.104:7003 192.168.1.101:7000
此时 Node-D 已经加入集群,但还没有分配任何槽位。
步骤二:重新分配槽位
bash
redis-cli --cluster reshard 192.168.1.101:7000
系统会询问:
- 要迁移多少个槽位?(比如 4096 个)
- 迁移到哪个节点?(Node-D 的 ID)
- 从哪些节点迁移?(可以选择 all,从所有节点平均迁移)
迁移后的槽位分布:
# 迁移前
Node-A: 0 ~ 5460 (5461 个槽)
Node-B: 5461 ~ 10922 (5462 个槽)
Node-C: 10923 ~ 16383 (5461 个槽)
# 迁移后(每个节点迁移约 1365 个槽给 Node-D)
Node-A: 1365 ~ 5460 (4096 个槽)
Node-B: 6826 ~ 10922 (4097 个槽)
Node-C: 12288 ~ 16383 (4096 个槽)
Node-D: 0~1364, 5461~6825, 10923~12287 (4096 个槽)
6.2 数据迁移过程
槽位迁移时,数据是怎么迁移的?
源节点 Node-A 目标节点 Node-D
┌─────────────┐ ┌─────────────┐
│ 槽位 0~1364 │ ──── 迁移 ────→ │ 槽位 0~1364 │
│ (迁移中) │ │ (接收中) │
└─────────────┘ └─────────────┘
迁移步骤:
-
标记槽位状态
- 源节点将槽位标记为
MIGRATING(迁出中) - 目标节点将槽位标记为
IMPORTING(迁入中)
- 源节点将槽位标记为
-
逐个迁移 Key
- 获取源节点该槽位的所有 Key
- 逐个将 Key 迁移到目标节点(MIGRATE 命令)
- 迁移是原子操作,要么成功要么失败
-
更新槽位归属
- 所有 Key 迁移完成后,更新槽位归属信息
- 通过 Gossip 协议通知所有节点
迁移期间的请求处理:
客户端请求 Key-X(属于正在迁移的槽位)
↓
访问源节点 Node-A
↓
Node-A 检查本地是否有 Key-X
├── 有 → 直接返回
└── 没有 → 返回 ASK 重定向到 Node-D
6.3 缩容流程
缩容就是扩容的逆过程:
- 将要下线节点的槽位迁移到其他节点
- 确认槽位迁移完成
- 将节点从集群中移除
bash
# 迁移槽位
redis-cli --cluster reshard 192.168.1.101:7000
# 移除节点
redis-cli --cluster del-node 192.168.1.101:7000 <node-id>
七、集群故障转移
7.1 主从架构
为了保证高可用,集群中的每个主节点都应该有从节点:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Master-A │ │ Master-B │ │ Master-C │
│ 槽位 0~5460 │ │槽位5461~10922│ │槽位10923~16383│
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
↓ ↓ ↓
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Slave-A │ │ Slave-B │ │ Slave-C │
└─────────────┘ └─────────────┘ └─────────────┘
7.2 故障检测
主观下线(PFAIL):
某个节点认为另一个节点不可用。
Node-A 向 Node-B 发送 PING
↓
超过 cluster-node-timeout 没收到 PONG
↓
Node-A 将 Node-B 标记为 PFAIL(主观下线)
客观下线(FAIL):
超过半数的主节点都认为某节点不可用。
Node-A 标记 Node-B 为 PFAIL
Node-C 标记 Node-B 为 PFAIL
Node-D 标记 Node-B 为 PFAIL
↓
超过半数主节点认为 Node-B 下线
↓
Node-B 被标记为 FAIL(客观下线)
↓
触发故障转移
7.3 故障转移过程
当主节点被标记为 FAIL 后,它的从节点会发起选举:
1. 从节点发现主节点 FAIL
2. 从节点向其他主节点发起投票请求
3. 其他主节点投票(每个主节点只能投一票)
4. 获得超过半数票的从节点成为新主节点
5. 新主节点接管原主节点的槽位
6. 广播通知所有节点更新配置
选举优先级:
- 复制偏移量大的从节点优先(数据更完整)
- 如果偏移量相同,节点 ID 小的优先
7.4 集群不可用的情况
以下情况会导致集群不可用:
| 情况 | 说明 |
|---|---|
| 某个槽位没有节点负责 | 主节点挂了且没有从节点 |
| 超过半数主节点挂了 | 无法完成故障转移投票 |
| 集群处于 FAIL 状态 | 需要人工介入修复 |
配置项 :cluster-require-full-coverage
yes(默认):任何槽位不可用,整个集群都不可用no:只有不可用槽位的数据无法访问,其他正常
八、集群模式的限制
8.1 不支持跨槽位的多 Key 操作
bash
# 如果 key1 和 key2 在不同槽位,以下命令会报错
MGET key1 key2
MSET key1 value1 key2 value2
解决方案:使用 Hash Tag 让相关 Key 落在同一槽位
bash
MGET {user:1001}:name {user:1001}:age # 可以执行
8.2 事务和 Lua 脚本的限制
事务(MULTI/EXEC)和 Lua 脚本中涉及的所有 Key 必须在同一个槽位。
bash
# 如果 key1 和 key2 在不同槽位,事务会失败
MULTI
SET key1 value1
SET key2 value2
EXEC
8.3 数据库只能使用 db0
单机 Redis 支持 16 个数据库(db0-db15),但集群模式只能使用 db0。
bash
SELECT 1 # 在集群模式下会报错
8.4 复制结构限制
- 不支持多层复制(从节点的从节点)
- 从节点只能复制主节点
8.5 Key 批量操作的性能
即使使用 Hash Tag,大量 Key 集中在一个槽位也会导致该节点压力过大(热点问题)。
九、总结
9.1 核心概念回顾
| 概念 | 说明 |
|---|---|
| 哈希槽 | 固定 16384 个,Key 通过 CRC16 映射到槽位 |
| 槽位分配 | 每个主节点负责一部分槽位 |
| MOVED | 槽位已永久迁移,客户端需更新缓存 |
| ASK | 槽位迁移中,临时重定向 |
| Gossip | 节点间去中心化通信协议 |
| PFAIL/FAIL | 主观下线/客观下线 |
9.2 集群模式 vs 主从复制 vs 哨兵
| 特性 | 主从复制 | 哨兵模式 | 集群模式 |
|---|---|---|---|
| 数据分片 | ❌ | ❌ | ✅ |
| 自动故障转移 | ❌ | ✅ | ✅ |
| 写入扩展 | ❌ | ❌ | ✅ |
| 读取扩展 | ✅ | ✅ | ✅ |
| 存储扩展 | ❌ | ❌ | ✅ |
9.3 集群模式适用场景
| 场景 | 是否适合 |
|---|---|
| 数据量超过单机内存 | ✅ 非常适合 |
| 需要高可用 | ✅ 适合 |
| 需要高写入吞吐 | ✅ 适合 |
| 大量跨 Key 操作 | ⚠️ 需要注意 Hash Tag |
| 复杂事务操作 | ❌ 不太适合 |
一句话总结:Redis 集群通过哈希槽实现数据分片,突破单机内存限制;通过主从复制实现高可用;通过 Gossip 协议实现去中心化管理。是 Redis 在大规模场景下的最佳实践方案。
热门专栏推荐
- Agent小册
- Java基础合集
- Python基础合集
- Go基础合集
- 大数据合集
- 前端小册
- 数据库合集
- Redis 合集
- Spring 全家桶
- 微服务全家桶
- 数据结构与算法合集
- 设计模式小册
- 消息队列合集
等等等还有许多优秀的合集在主页等着大家的光顾,感谢大家的支持
文章到这里就结束了,如果有什么疑问的地方请指出,诸佬们一起来评论区一起讨论😊
希望能和诸佬们一起努力,今后我们一起观看感谢您的阅读🙏
如果帮助到您不妨3连支持一下,创造不易您们的支持是我的动力🌟