第 4 章:Redis 集群
学习目标
- 理解 Redis Cluster 的分片原理
- 搭建一个 3 主 3 从的本地集群
- 掌握 Java 连接 Cluster
- 处理集群扩容、数据迁移、热点 key 问题
一、为什么需要集群
单实例 Redis 瓶颈:
| 瓶颈 | 表现 |
|---|---|
| 内存 | 单机最大受物理内存限制 |
| 写性能 | 单线程写,QPS 上限 ~10 万 |
| 高可用 | 单点故障,挂了服务就停 |
集群方案对比:
| 方案 | 特点 |
|---|---|
| 主从复制 | 读写分离,不解决容量 |
| Sentinel | 自动故障转移,不解决容量 |
| Cluster | 分片 + 复制 + 自动故障转移,生产首选 |
三种方案通俗理解:
- 主从复制 = 一份文件做多份备份(原件 + 复印件),原件坏了手动换
- Sentinel = 自动切换的备用电源,原件坏了自动切到备份
- Cluster = 多个分店,数据分摊到各家,每家又有自己的备份(主从 + 自动故障转移)
核心区别:Sentinel 只解决高可用(自动切换),不解决容量上限。数据量大了必须 Cluster。**
二、Cluster 核心原理
2.0 一句话理解(通俗版)
Redis 集群 = 把 1 个 Redis 拆成多个 Redis,合起来用,既能存更多数据,又能扛更高并发。
单 Redis 的瓶颈(为啥要拆):
单 Redis 像一家独门独户的小卖部——货架(内存)就那么大,只有一个收银台(单线程),店塌了服务就停了。数据量大了、并发高了,这一家店扛不住。
集群的解决思路:不是开更大的店,而是开分店:
- 3 家店 = 3 倍容量
- 3 家店同时接单 = 3 倍速度
- 1 家塌了,另外几家顶上(高可用)
你只管 SET key value,Redis 自动算该放哪家店,你完全不用关心。
2.1 哈希槽(Hash Slot)
Redis Cluster 把数据分成 16384 个槽,每个 key 通过 CRC16(key) % 16384 落到对应槽:
key = "user:1001"
slot = CRC16("user:1001") % 16384 = 5460每个节点负责一部分槽(例:3 主 3 从,每个主节点 ~5461 个槽)。
通俗类比(抽屉号):
- 16384 = 一栋大楼的总房间号(抽屉号)
- 3 家店 = 3 个楼层,每层管 ~5461 个房间
- key = 快递包裹,系统自动算"房间号" → 自动送到对应楼层
整张集群的分工:
┌────────┐
│ 客户端 │ SET user:1001 "tom"
└────┬───┘ 自动算 hash 槽 = 5460
│
▼ 自动路由
┌────────┼────────┐
▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐
│ 店 A │ │ 店 B │ │ 店 C │ 3 主 3 从
│ 主+从│ │ 主+从│ │ 主+从│
│ 槽 │ │ 槽 │ │ 槽 │
│0~5460│ │5461~ │ │10922~│
└──────┘ │10921 │ │16383 │
└──────┘ └──────┘
▲
│
user:1001 → 5460 → 落店 A⚠️ 坑 1:
KEYS *、MSET、BITOP等多 key 命令在集群下要求所有 key 在同一槽,否则报错。需要用 Hash Tag:{user}:1001和{user}:2001会落到同一槽({}内才参与 hash)。
2.2 主从复制
基础主从复制本身不会自动切换——主挂了,需要 Sentinel(哨兵)来监控并触发切换;Cluster 集群内置了类似 Sentinel 的自动故障转移机制,主挂了集群内的节点通过投票自动选新主。
| 方案 | 主挂了怎么办 |
|---|---|
| 纯主从复制 | ❌ 不会自动升级,需要人工介入 |
| Sentinel | ✅ 哨兵投票自动切换 |
| Cluster | ✅ 集群内置故障转移(类似 Sentinel) |
通俗版(正店 + 备胎):
店 A(主) ←──店 A'(从,实时抄作业)
店 B(主) ←──店 B'(从,实时抄作业)
店 C(主) ←──店 C'(从,实时抄作业)- 平时:主店接客,从店同步数据(备用)
- 主店塌了:在 Sentinel/Cluster 下,从店几秒内顶上变新主,用户无感
- 在裸主从下:得人工手动指定哪个从顶上
这就是常说的 3 主 3 从——主从拓扑,故障转移机制取决于用哪种方案。
三种方案的区别:
| 方案 | 解决了啥 | 没解决啥 |
|---|---|---|
| 主从复制 | 读写分离 | 容量上限、自动切换 |
| Sentinel | 自动故障转移 | 容量上限 |
| Cluster | 容量 + 性能 + 高可用,全解决 | - |
2.3 故障转移
节点间通过 Gossip 协议互相通信,半数以上主节点认为某主"主观下线",触发选举,新主接管。
通俗版(公司主管打比方):
3 个主管(主节点)互相发微信沟通(Gossip)。
主管 A 突然不回微信
↓
主管 B 多次联系 A → 没回复 → "主管 A 主观下线"(单方面判断)
主管 C 多次联系 A → 也没回复 → "我也觉得 A 挂了"
↓
2 个主管(>半数)都觉得 A 挂了 → "客观下线"
↓
触发投票 → 主管 B 中票 → 升级为新主
↓
原来 A 的工作转给 B,员工(用户)啥也不知道关键词:
| 词 | 啥意思 |
|---|---|
| Gossip 协议 | 节点之间互相发消息(像公司八卦一样传播) |
| 主观下线(SDOWN) | 一个节点觉得主挂了(单方面判断) |
| 客观下线(ODOWN) | 半数以上主节点都觉得它挂了(多数票) |
| 触发选举 | 大家投票选新主 |
| 新主接管 | 中票最多的顶上 |
为啥要"半数"? 防止网络抽风被误判——1 个人没回可能只是忙/网卡,大多数人都没回才确认是真出事。
三、搭建本地 Cluster(Docker)
# 启动 6 个 Redis 容器(3 主 3 从)
for port in 7000 7001 7002 7003 7004 7005; do
docker run -d --name redis-$port \
-p $port:6379 \
--net redis-net \
redis:7-alpine \
redis-server --cluster-enabled yes \
--port 6379 \
--cluster-config-file nodes.conf
done
# 创建集群(在某个容器内执行)
docker exec -it redis-7000 redis-cli --cluster create \
127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \
127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \
--cluster-replicas 1按提示输入 yes,集群创建完成。
关键参数讲解
| 参数 | 干啥 |
|---|---|
-p $port:6379 | 端口映射——外面 $port(7000/7001...)进来 → 转给容器内 6379 |
--net redis-net | 让6 个容器在同一个网络,能互相找到 |
redis:7-alpine | 用 Redis 7 轻量版镜像 |
--port 6379 | 容器内部 Redis 监听的端口(默认就是 6379) |
--cluster-config-file nodes.conf | 集群"小本本",记节点信息和槽位分配 |
docker exec -it redis-7000 | 只挑 1 个容器进去跑命令,redis-7000 不是"主",只是借它的 redis-cli 工具 |
--cluster-replicas 1 | 每个主配 1 个从(6 个 → 自动 3 主 3 从) |
--port 6379 跟 -p $port:6379 必须对应——Redis 在容器内听 6379,外面才能通过映射连进来;不对应就连不上。
谁是主、谁是从?
Redis 按你列的顺序自动分:前 3 个是主,后 3 个是从,按顺序一一配对。
7000 7001 7002 7003 7004 7005
↓ ↓ ↓ ↓ ↓ ↓
主1 主2 主3 从1 从2 从3
↓ ↓ ↓
配主1 配主2 配主3⚠️ 简单场景靠顺序就行,想自己指定得用更复杂的命令。
查看集群状态
docker exec -it redis-7000 redis-cli -p 6379 CLUSTER NODES
docker exec -it redis-7000 redis-cli -p 6379 CLUSTER INFO⚠️ 坑 2:Cluster 节点必须开启
cluster-enabled yes,否则redis-cli -c找不到集群信息。
四、客户端路由
4.1 最简单写法(99% 项目用这个)
Spring Boot 有自动配置,不用写 Java 代码,只需要两步:
第 1 步:加依赖(pom.xml)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>第 2 步:application.yml 写 3 行配置
spring:
data:
redis:
cluster:
nodes:
- 127.0.0.1:7000
- 127.0.0.1:7001
- 127.0.0.1:7002
max-redirects: 3业务代码直接注入 RedisTemplate,key 自动路由到对的节点:
@Autowired
private StringRedisTemplate redis;
redis.opsForValue().set("user:1001", "tom"); // 自动算 hash 槽 → 自动找对节点Spring Boot 自动帮你干:
- 用 Lettuce 连
- 读 yml 的 3 个节点
- 自动发现其他从节点
- 自动算 hash 槽、路由 key
4.2 为啥只填 3 个入口?
不是"只连这 3 个",而是"从这 3 个入口去发现整个集群"。
你填 7000/7001/7002
↓
Lettuce 连上 7000
↓
问 7000: "集群里还有谁?"
↓
7000 告诉它: "还有 7001、7002(主) + 7003、7004、7005(从)"
↓
Lettuce 自动维护全部 6 个节点的连接惯例填 3 个主,也能填 1 个或任意 3 个(从也行),Lettuce 都能自动发现。
4.3 max-redirects: 3 是啥?
最多允许客户端追 3 次重定向(节点间跳转)。
重定向场景:
你: GET user:1001 → 连 A 节点
↓
A: 这个 key 不在我这,去 C 节点(端口 7002)
↓
你去 C 拿数据 ✅(1 次重定向)为啥是 3? 正常 1 次够,数据迁移中可能 2 次,3 是安全默认值,防止在坏集群里死循环。
| 数字 | 行为 |
|---|---|
| 1 | 太小,迁移时会报错 |
| 3 | 默认推荐,覆盖 99% 场景 |
| 太大 | 在坏集群上浪费时间 |
4.4 自定义 Java 配置(进阶)
下面是进阶写法,只有需要定制时才用(自定义序列化、连接池、读策略等):
@Configuration
public class ClusterConfig {
@Bean
public RedisConnectionFactory clusterFactory() {
List<RedisNode> nodes = List.of(
new RedisNode("127.0.0.1", 7000),
new RedisNode("127.0.0.1", 7001),
new RedisNode("127.0.0.1", 7002)
);
RedisClusterConfiguration cfg = new RedisClusterConfiguration(nodes);
return new LettuceConnectionFactory(cfg);
}
}| 场景 | 要不要写 Java 配置 |
|---|---|
| 普通读写 | ❌ yml 就行 |
| 读优先从从节点 | ❌ yml 加 read-from: master_preferred |
| 自定义序列化、连接池 | ✅ 要写 |
⚠️ 坑 3:
Lettuce默认所有命令走主节点;要读从节点,需要单独配置LettuceClientConfiguration+ReadFrom.MASTER_PREFERRED(Spring Boot 3.x 在application.yml配spring.data.redis.lettuce.cluster.read-from)。
五、Hash Tag 实战
为啥要 Hash Tag?(通俗版)
正常情况下不同 key 落不同店,但有时你希望多个 key 必须在同一家店(比如"扣库存 + 写订单"要原子操作)。
用 {} 强制同槽:{} 里内容才参与 hash,所以 {user}:xxx 全都落同一个槽。
MSET 是什么:一次性设多个 key-value(M = Multiple,SET = 设值):
# 普通 SET 一次设一个
SET user:1001 "tom"
SET user:1002 "jerry"
# MSET 一次设多个,成对出现:key1 value1 key2 value2 ...
MSET user:1001 "tom" user:1002 "jerry" user:1003 "alice"为啥集群下会报错 CROSSSLOT?
用银行转账打比方——MSET = 在同一家银行同时办多件事:
MSET user:1001 "tom" user:1002 "jerry"
↓ ↓
槽 5460 槽 8888
节点 A 节点 B
一条 MSET 命令不可能同时跑在 A 和 B 两家银行
→ 报错 CROSSSLOT(跨节点槽)解决:用 Hash Tag 强制多个 key 落同一个槽(同一家银行):
MSET {user}:1001 "tom" {user}:1002 "jerry"
↓ ↓
槽 5460 槽 5460 ← 强制同槽,一次执行 ✅类似的"多 key 命令"还有 MGET / KEYS * / BITOP,集群下都要求同槽。
# 不同 user 的 key 落在不同槽(分布式)
SET user:1001 "tom" → slot 5460
SET user:1002 "jerry" → slot 1234
# 用 {} 强制同一槽(原子操作多 key)
SET {user}:1001 "tom" → slot 5460
SET {user}:1002 "jerry" → slot 5460 ← 同一槽Java 场景:扣库存 + 写订单 + 删购物车 → 用 Hash Tag 保证同槽:
String orderKey = "{order:" + userId + "}:detail"; // 所有 user 的订单相关都同槽六、扩容与数据迁移
# ① 加新节点(7006 是新人,7000 是介绍人)
# 7000 帮 7006 在集群里登记,但此时 7006 还没槽位(空架子)
redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000
# ② 重新分配槽位(从老节点搬槽位给新人)
# --cluster-from ← 从哪个老节点搬
# --cluster-to ← 搬到哪个新节点
# --cluster-slots ← 搬多少个槽
redis-cli --cluster reshard 127.0.0.1:7000 \
--cluster-from <src-node-id> \
--cluster-to <dst-node-id> \
--cluster-slots 1000两步走的顺序:先 add-node 加人(空架子)→ 再 reshard 搬槽位(有数据)。
MOVED vs ASK(客户端怎么处理):
| 响应 | 啥意思 | 类比 |
|---|---|---|
MOVED 槽号 IP:端口 | 永远搬走了,直接去新节点 | "小明已搬家,新家在 5 号楼" |
ASK 槽号 IP:端口 | 正在搬,临时去问一下 | "小明正在搬家,可能在老房或新房" |
客户端(Lettuce)自动处理,业务代码零修改。
⚠️ 坑 4:reshard 期间单 key 迁移是阻塞的(几十 ms),大 Key(>1MB)会拖慢请求 —— 提前拆分大 key。
七、热点 Key 问题
单个 key QPS 极高(明星离婚、秒杀商品),所有请求打到同一节点,形成热点。
解决方案:
- 多级缓存:本地缓存(Caffeine)+ Redis,本地扛大部分
- Key 分散:同一逻辑 key 写多份,随机读取java
int shard = ThreadLocalRandom.current().nextInt(5); String key = "article:hot:" + shard; - 读写分离:从节点分担读压力
八、本章小结
| 要点 | 关键 |
|---|---|
| 集群分片 | 16384 个槽,CRC16 取模 |
| Hash Tag | {} 强制同槽,支持多 key 原子操作 |
| 主从复制 | 每个主挂 N 从,自动故障转移 |
| 扩容 | add-node + reshard,透明迁移 |
| 热点 Key | 本地缓存 + Key 分散 + 读写分离 |
| 客户端 | Lettuce 自动路由,无需手动选节点 |
动手练习
- 用 Docker Compose 启动 3 主 3 从 Redis Cluster
- 用
redis-cli -c写入数据,观察不同 key 落到不同槽 - 用 Hash Tag 让某用户的所有数据同槽,验证
MGET能成功
下一章:第 5 章:Redis 实战 →