Skip to content
第 4 章 ⏱ 14 分钟阅读

第 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 落到对应槽:

text
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) ​

bash
# 启动 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

⚠️ 简单场景靠顺序就行,想自己指定得用更复杂的命令。

查看集群状态 ​

bash
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)

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

第 2 步:application.yml 写 3 行配置

yaml
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 自动路由到对的节点:

java
@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 配置(进阶) ​

下面是进阶写法,只有需要定制时才用(自定义序列化、连接池、读策略等):

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 = 设值):

bash
# 普通 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 落同一个槽(同一家银行):

bash
MSET {user}:1001 "tom" {user}:1002 "jerry"
       ↓              ↓
     槽 5460        槽 5460  ← 强制同槽,一次执行 ✅

类似的"多 key 命令"还有 MGET / KEYS * / BITOP,集群下都要求同槽。

bash
# 不同 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 保证同槽:

java
String orderKey = "{order:" + userId + "}:detail";   // 所有 user 的订单相关都同槽

六、扩容与数据迁移 ​

bash
# ① 加新节点(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 极高(明星离婚、秒杀商品),所有请求打到同一节点,形成热点。

解决方案:

  1. 多级缓存:本地缓存(Caffeine)+ Redis,本地扛大部分
  2. Key 分散:同一逻辑 key 写多份,随机读取
    java
    int shard = ThreadLocalRandom.current().nextInt(5);
    String key = "article:hot:" + shard;
  3. 读写分离:从节点分担读压力

八、本章小结 ​

要点关键
集群分片16384 个槽,CRC16 取模
Hash Tag{} 强制同槽,支持多 key 原子操作
主从复制每个主挂 N 从,自动故障转移
扩容add-node + reshard,透明迁移
热点 Key本地缓存 + Key 分散 + 读写分离
客户端Lettuce 自动路由,无需手动选节点

动手练习 ​

  1. 用 Docker Compose 启动 3 主 3 从 Redis Cluster
  2. 用 redis-cli -c 写入数据,观察不同 key 落到不同槽
  3. 用 Hash Tag 让某用户的所有数据同槽,验证 MGET 能成功

下一章:第 5 章:Redis 实战 →

本站基于 VitePress 构建 · 由 StackHub 团队维护