Redis集群模式架构详解
前言
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命令获取槽位分配信息:
127.0.0.1:7000>CLUSTER SLOTS1)1)(integer)02)(integer)54603)1)"192.168.1.101"2)(integer)70002)1)(integer)54612)(integer)109223)1)"192.168.1.102"2)(integer)70013)1)(integer)109232)(integer)163833)1)"192.168.1.103"2)(integer)7002客户端会在本地缓存这份槽位映射表,后续请求直接根据 Key 计算槽位,找到对应节点。
5.2 MOVED 重定向
如果客户端的槽位信息过期了,访问了错误的节点,会收到MOVED错误:
# 客户端访问 Node-A,但 Key 实际在 Node-C127.0.0.1:7000>GET product:10086(error)MOVED12072192.168.1.103:7002MOVED 的含义:
12072:这个 Key 对应的槽位编号192.168.1.103:7002:负责这个槽位的节点地址
客户端收到 MOVED 后会:
- 更新本地的槽位映射表
- 重新向正确的节点发送请求
打个比方:你去政务大厅办事,走错了窗口,工作人员告诉你"这个业务请去 3 号窗口",你就去 3 号窗口,下次再办同样的事就直接去 3 号窗口了。
5.3 ASK 重定向
ASK是另一种重定向,发生在数据迁移过程中。
127.0.0.1:7000>GET product:10086(error)ASK12072192.168.1.103:7002ASK 与 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。
步骤一:将新节点加入集群
redis-cli --cluster add-node192.168.1.104:7003192.168.1.101:7000此时 Node-D 已经加入集群,但还没有分配任何槽位。
步骤二:重新分配槽位
redis-cli --cluster reshard192.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-D6.3 缩容流程
缩容就是扩容的逆过程:
- 将要下线节点的槽位迁移到其他节点
- 确认槽位迁移完成
- 将节点从集群中移除
# 迁移槽位redis-cli --cluster reshard192.168.1.101:7000# 移除节点redis-cli --cluster del-node192.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 操作
# 如果 key1 和 key2 在不同槽位,以下命令会报错MGET key1 key2 MSET key1 value1 key2 value2解决方案:使用 Hash Tag 让相关 Key 落在同一槽位
MGET{user:1001}:name{user:1001}:age# 可以执行8.2 事务和 Lua 脚本的限制
事务(MULTI/EXEC)和 Lua 脚本中涉及的所有 Key 必须在同一个槽位。
# 如果 key1 和 key2 在不同槽位,事务会失败MULTI SET key1 value1 SET key2 value2 EXEC8.3 数据库只能使用 db0
单机 Redis 支持 16 个数据库(db0-db15),但集群模式只能使用 db0。
SELECT1# 在集群模式下会报错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连支持一下,创造不易您们的支持是我的动力🌟