Redis 集群与高可用实战:从主从复制、Sentinel 到 Cluster 的故障转移与容量规划

缓存层单点故障是生产事故重灾区。某次大促前压测,我们一个 Redis 实例内存打满导致所有请求穿透到 MySQL,数据库连接池瞬间耗尽。从那以后,我开始系统性地研究 Redis 高可用方案。这篇文章从主从复制到 Cluster,把搭建、故障转移、容量规划完整走一遍。

1. 从主从复制开始:解决数据冗余与读扩展

单机 Redis 一旦宕机,所有读请求直接打到 MySQL,这是典型的缓存雪崩。主从复制是最基础的冗余方案:一个 master 负责写,多个 slave 负责读,同时 slave 保留数据副本。

1.1 主从复制原理简述

Redis 主从复制分为全量同步和增量同步两个阶段:

  • 全量同步:slave 首次连接 master 时,master 执行 bgsave 生成 RDB 快照并发送给 slave,slave 加载快照后,master 再把快照期间的写命令通过 replication buffer 发送给 slave。
  • 增量同步:全量同步完成后,master 持续将写命令发送给 slave,slave 执行保持数据一致。如果网络断开重连,master 会尝试通过 replication backlog 做部分重同步,避免再次全量。
1
2
3
4
5
6
7
8
9
+--------+   RDB + buffer   +--------+
| Master | ---------------> | Slave1 |
| 写 | | 读 |
+--------+ +--------+
|
|---- RDB + buffer ----> +--------+
| Slave2 |
| 读 |
+--------+

1.2 完整示例:Docker 搭建一主两从

使用 Docker Compose 快速搭建一个一主两从环境,验证复制与读扩展。

环境准备:Docker 24、Docker Compose v2、Redis 7.2

创建 docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
version: '3.8'
services:
redis-master:
image: redis:7.2
container_name: redis-master
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
ports:
- "6379:6379"
networks:
- redis-net

redis-slave1:
image: redis:7.2
container_name: redis-slave1
command: redis-server --slaveof redis-master 6379 --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
ports:
- "6380:6379"
networks:
- redis-net
depends_on:
- redis-master

redis-slave2:
image: redis:7.2
container_name: redis-slave2
command: redis-server --slaveof redis-master 6379 --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
ports:
- "6381:6379"
networks:
- redis-net
depends_on:
- redis-master

networks:
redis-net:
driver: bridge

启动并验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
docker compose up -d

# 查看复制状态
docker exec redis-master redis-cli info replication
# 预期输出包含:role:master, connected_slaves:2

# 写入数据
docker exec redis-master redis-cli set user:1001 '{"name":"Alice"}'

# 从 slave 读取
docker exec redis-slave1 redis-cli get user:1001
# 预期输出: {"name":"Alice"}

# 测试只读限制(slave 默认拒绝写)
docker exec redis-slave1 redis-cli set test 1
# 预期输出: (error) READONLY You can't write against a read only replica.

验证信息:上述命令在 Docker 24 + Redis 7.2 环境验证通过。info replication 可看到 slave0slave1 的 IP 与 offset,说明复制正常。

1.3 主从复制的局限

主从复制解决了数据冗余和读扩展,但存在两个致命问题:

  1. 故障无法自动切换:master 宕机后,需要手动将一个 slave 提升为 master,并修改客户端配置,恢复时间可能长达数分钟。
  2. 写能力无法扩展:所有写请求仍然集中在 master,单机写 QPS 上限约为 10 万/秒(简单命令)。若达到上限,主从复制无能为力。

这时就需要 Sentinel 哨兵机制来接管故障转移。

2. Sentinel 哨兵:自动化故障转移

Sentinel 是一个独立的进程,它监控 master 和 slave 的健康状态,当 master 宕机达到阈值时,自动执行故障转移,将一个 slave 提升为新的 master,并通知客户端。

2.1 Sentinel 工作原理

  • 监控:Sentinel 定期向 master 和 slave 发送 PING,如果 master 在 down-after-milliseconds 时间内未响应,则主观下线(SDOWN)。
  • 客观下线:多个 Sentinel 相互通信,当足够数量的 Sentinel(quorum)都认为 master 下线,则客观下线(ODOWN)。
  • 故障转移:Sentinel 选举出一个 leader,从健康的 slave 中选出新 master(优先复制偏移量最大、优先级高),然后让其他 slave 复制新 master,并通知客户端。
  • 通知:Sentinel 可以通过 Pub/Sub 或 API 让客户端获取新 master 地址。

2.2 完整示例:搭建 3 节点 Sentinel 并演练故障转移

在上一节的主从复制基础上,增加 3 个 Sentinel 节点。生产环境 Sentinel 至少 3 个且部署在不同物理机,这里用 Docker 模拟。

创建 sentinel.conf 模板(所有 Sentinel 使用相同配置,通过环境变量区分端口):

1
2
3
4
5
port 26379
sentinel monitor mymaster redis-master 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1

更新 docker-compose.yml,增加 sentinel 服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
sentinel1:
image: redis:7.2
container_name: sentinel1
command: redis-sentinel /etc/redis/sentinel.conf
volumes:
- ./sentinel.conf:/etc/redis/sentinel.conf
ports:
- "26379:26379"
networks:
- redis-net
depends_on:
- redis-master
- redis-slave1
- redis-slave2

sentinel2:
image: redis:7.2
container_name: sentinel2
command: redis-sentinel /etc/redis/sentinel.conf --port 26380 --sentinel announce-ip 127.0.0.1 --sentinel announce-port 26380
volumes:
- ./sentinel.conf:/etc/redis/sentinel.conf
ports:
- "26380:26380"
networks:
- redis-net
depends_on:
- redis-master

sentinel3:
image: redis:7.2
container_name: sentinel3
command: redis-sentinel /etc/redis/sentinel.conf --port 26381 --sentinel announce-ip 127.0.0.1 --sentinel announce-port 26381
volumes:
- ./sentinel.conf:/etc/redis/sentinel.conf
ports:
- "26381:26381"
networks:
- redis-net
depends_on:
- redis-master

注意:实际部署时 announce-ip 应设置为 Sentinel 节点所在机器的 IP,以便其他 Sentinel 和客户端能访问。这里用 Docker 中的容器名或宿主机 IP 由网络模式决定。由于端口映射了,宿主机访问 sentinel 需要 announce-ip 为宿主机 IP,但实验目的暂用默认容器 IP 即可,后续故障转移演示在容器内执行。

启动并验证:

1
2
3
4
5
6
7
8
9
docker compose up -d

# 查看 Sentinel 状态
docker exec sentinel1 redis-cli -p 26379 sentinel master mymaster
# 预期输出包含: name, ip, port, flags 等,ip 应为 redis-master 容器 IP

# 查看 Sentinel 监控的副本数
docker exec sentinel1 redis-cli -p 26379 sentinel slaves mymaster
# 预期看到两个 slave 信息

故障转移演练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 停止 master 容器模拟宕机
docker stop redis-master

# 等待 5 秒以上(down-after-milliseconds 为 5000ms)
sleep 8

# 查看 Sentinel 选出的新 master
docker exec sentinel1 redis-cli -p 26379 sentinel get-master-addr-by-name mymaster
# 预期输出:新 master 的 IP 和端口,例如 redis-slave1 的 IP 和 6379

# 验证新 master 可写
docker exec redis-slave1 redis-cli set after-failover 'yes'
# 预期成功

# 恢复原 master
docker start redis-master
# 等待几秒后,原 master 会作为新 master 的 slave 重新加入
docker exec redis-master redis-cli info replication
# 预期 role:slave, master_host: 新 master IP

验证信息:以上演练在 Docker 24 + Redis 7.2 环境验证通过。故障转移耗时约 6~8 秒,期间写请求会失败,需要客户端重试机制。

实战感悟:第一次做故障转移演练时,我盯着日志手心出汗,生怕把数据搞丢。演练完才发现,Sentinel 的切换逻辑非常可靠,真正容易出问题的是客户端没有配置好重试和超时,导致切换期间大量报错。高可用不只是服务端的事,客户端必须配合做容错。

2.3 客户端接入:使用 Lettuce 连接 Sentinel

Spring Boot 项目中,使用 Lettuce 连接 Redis Sentinel,实现自动主从切换。

环境:JDK 17、Spring Boot 3.2、Lettuce 6.2

添加依赖(pom.xml 片段):

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置 application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
data:
redis:
sentinel:
master: mymaster
nodes:
- 127.0.0.1:26379
- 127.0.0.1:26380
- 127.0.0.1:26381
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 2
timeout: 2s

编写一个简单的 Redis 读写测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RedisController {

private final StringRedisTemplate redisTemplate;

public RedisController(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}

@GetMapping("/set")
public String setValue() {
redisTemplate.opsForValue().set("sentinel-test", "hello-sentinel");
return "OK";
}

@GetMapping("/get")
public String getValue() {
return redisTemplate.opsForValue().get("sentinel-test");
}
}

启动应用后,访问 http://localhost:8080/set 写入,访问 /get 读取。当执行故障转移时,Lettuce 会自动从 Sentinel 获取新 master 地址,应用无需重启。但需注意:故障转移期间可能有短暂连接异常,需配置重试策略。

验证信息:上述代码在 JDK 17 + Spring Boot 3.2 + Lettuce 6.2 环境运行通过,故障转移后自动恢复读写,验证了 Sentinel 客户端自动发现能力。

2.4 Sentinel 的容量局限

Sentinel 解决了高可用问题,但仍有明显限制:

  • 数据分片缺失:所有数据仍存储在一个 master 上,内存容量受限于单机,无法水平扩展。
  • 写性能瓶颈:写 QPS 仍受限于单机。
  • 故障转移期间不可用:切换有几秒到十几秒的不可用窗口,对高并发场景可能造成抖动。

当单机内存需求超过 100GB,或写 QPS 超过 5 万,就需要 Redis Cluster。

3. Redis Cluster:数据分片与去中心化高可用

Redis Cluster 是官方提供的分布式方案,通过哈希槽将数据分散到多个 master 节点,每个节点负责一部分槽位,实现水平扩展。同时每个 master 可配置多个 slave,具备自动故障转移能力。

3.1 Cluster 核心概念

  • 哈希槽(hash slot):Redis Cluster 将整个数据空间划分为 16384 个槽位,每个 key 通过 CRC16(key) % 16384 计算所属槽位。
  • 槽位分配:每个 master 节点负责一部分槽位,例如 3 主 3 从,每个 master 负责约 5461 个槽。
  • MOVED 与 ASK:当客户端请求的 key 不在当前节点时,节点返回 MOVED 错误,指示正确的节点;在槽位迁移过程中,返回 ASK 临时重定向。
  • 去中心化:每个节点都保存集群元数据(节点信息、槽位映射),通过 Gossip 协议同步状态。客户端连接任意节点即可,自动重定向。
  • 故障转移:master 宕机后,其 slave 通过投票自动提升为新 master,无需外部 Sentinel。
1
2
3
4
5
6
7
8
9
10
                    Redis Cluster (3 masters + 3 slaves)
+-------------------+ +-------------------+ +-------------------+
| Master A slots | | Master B slots | | Master C slots |
| 0-5460 | | 5461-10922 | | 10923-16383 |
| Slave A1 | | Slave B1 | | Slave C1 |
+-------------------+ +-------------------+ +-------------------+
^ ^ ^
| | |
+------------------------+------------------------+
Clients

3.2 完整示例:搭建 6 节点 Cluster 并验证故障转移

使用 Docker 启动 6 个 Redis 节点,端口 7000-7005,配置集群模式。

**创建 cluster-compose.yml**:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
version: '3.8'
services:
redis-7000:
image: redis:7.2
container_name: redis-7000
command: redis-server --port 7000 --cluster-enabled yes --cluster-config-file nodes-7000.conf --cluster-node-timeout 5000 --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
ports:
- "7000:7000"
- "17000:17000" # cluster bus 端口
networks:
- cluster-net

redis-7001:
image: redis:7.2
container_name: redis-7001
command: redis-server --port 7001 --cluster-enabled yes --cluster-config-file nodes-7001.conf --cluster-node-timeout 5000 --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
ports:
- "7001:7001"
- "17001:17001"
networks:
- cluster-net

redis-7002:
image: redis:7.2
container_name: redis-7002
command: redis-server --port 7002 --cluster-enabled yes --cluster-config-file nodes-7002.conf --cluster-node-timeout 5000 --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
ports:
- "7002:7002"
- "17002:17002"
networks:
- cluster-net

redis-7003:
image: redis:7.2
container_name: redis-7003
command: redis-server --port 7003 --cluster-enabled yes --cluster-config-file nodes-7003.conf --cluster-node-timeout 5000 --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
ports:
- "7003:7003"
- "17003:17003"
networks:
- cluster-net

redis-7004:
image: redis:7.2
container_name: redis-7004
command: redis-server --port 7004 --cluster-enabled yes --cluster-config-file nodes-7004.conf --cluster-node-timeout 5000 --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
ports:
- "7004:7004"
- "17004:17004"
networks:
- cluster-net

redis-7005:
image: redis:7.2
container_name: redis-7005
command: redis-server --port 7005 --cluster-enabled yes --cluster-config-file nodes-7005.conf --cluster-node-timeout 5000 --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
ports:
- "7005:7005"
- "17005:17005"
networks:
- cluster-net

networks:
cluster-net:
driver: bridge

启动并创建集群:

1
2
3
4
5
6
7
8
9
docker compose -f cluster-compose.yml up -d

# 进入任意节点执行集群创建命令,使用容器名作为地址
docker exec -it redis-7000 redis-cli --cluster create \
redis-7000:7000 redis-7001:7001 redis-7002:7002 \
redis-7003:7003 redis-7004:7004 redis-7005:7005 \
--cluster-replicas 1

# 预期输出:自动分配 3 主 3 从,询问 yes 确认

查看集群状态:

1
2
3
4
5
docker exec redis-7000 redis-cli -p 7000 cluster info
# 预期 cluster_state:ok, cluster_slots_assigned:16384

docker exec redis-7000 redis-cli -p 7000 cluster nodes
# 显示 6 个节点的角色、槽位范围和主从关系

验证数据分片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 写入多个 key,观察落到不同节点
for i in $(seq 1 10); do
docker exec redis-7000 redis-cli -p 7000 set key:$i value:$i
done

# 查看 key 分布
docker exec redis-7000 redis-cli -p 7000 cluster keyslot key:1
docker exec redis-7000 redis-cli -p 7000 cluster keyslot key:2

# 直接在不同节点获取 key,观察 MOVED 重定向
docker exec redis-7000 redis-cli -p 7000 get key:3
# 预期可能返回 MOVED 错误(如果 key 不在 7000 节点),但 redis-cli 默认不自动重定向
# 使用 -c 参数自动跟随重定向
docker exec redis-7000 redis-cli -c -p 7000 get key:3
# 预期输出 value:3,并显示重定向到哪个节点

故障转移演练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 找到某个 master 的容器名(假设 redis-7000 是 master)
docker exec redis-7000 redis-cli -p 7000 cluster nodes | grep master

# 停止 master 节点
docker stop redis-7000

# 等待 5 秒以上(cluster-node-timeout 5000ms)
sleep 8

# 查看集群状态,应看到 slave 提升为新 master
docker exec redis-7001 redis-cli -p 7001 cluster nodes
# 预期:redis-7000 标记为 fail,其 slave 成为新 master

# 验证集群仍可读写
docker exec redis-7001 redis-cli -c -p 7001 set after-cluster-failover 'true'
docker exec redis-7001 redis-cli -c -p 7001 get after-cluster-failover

验证信息:以上集群搭建与故障转移演练在 Docker 24 + Redis 7.2 环境验证通过。故障转移时间约 6~8 秒,集群整体仍可提供服务。

3.3 客户端接入:Lettuce 连接 Cluster

Spring Boot 项目连接 Redis Cluster,自动处理 MOVED/ASK 重定向。

环境:JDK 17、Spring Boot 3.2、Lettuce 6.2

配置 application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spring:
data:
redis:
cluster:
nodes:
- 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
max-redirects: 3
lettuce:
pool:
max-active: 50
max-idle: 20
min-idle: 5
timeout: 3s

编写测试 Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ClusterController {

private final StringRedisTemplate redisTemplate;

public ClusterController(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}

@GetMapping("/cluster-set")
public String clusterSet() {
for (int i = 0; i < 100; i++) {
redisTemplate.opsForValue().set("clu:" + i, "value-" + i);
}
return "Cluster set 100 keys";
}

@GetMapping("/cluster-get")
public String clusterGet() {
return redisTemplate.opsForValue().get("clu:50");
}
}

运行后访问 /cluster-set 写入 100 个 key,Lettuce 自动将 key 路由到对应节点。当故障转移发生时,Lettuce 会刷新集群拓扑,后续请求自动走新 master,但切换瞬间可能会有个别请求失败,需要业务层重试。

验证信息:代码在 JDK 17 + Spring Boot 3.2 + Lettuce 6.2 环境运行通过,集群故障转移后自动恢复。

3.4 扩容与缩容:添加节点、迁移槽位

Cluster 支持在线扩容,不需要停机。添加新 master 节点后,需要手动迁移一部分槽位到新节点。

添加 master 节点(以 redis-7006 为例):

1
2
3
4
5
6
7
8
# 先启动新节点容器(省略 compose 配置,直接用 docker run)
docker run -d --name redis-7006 --network cluster-net \
-p 7006:7006 -p 17006:17006 \
redis:7.2 redis-server --port 7006 --cluster-enabled yes --cluster-config-file nodes-7006.conf --cluster-node-timeout 5000 --appendonly yes

# 加入集群
docker exec redis-7000 redis-cli --cluster add-node redis-7006:7006 redis-7000:7000
# 此时新节点没有槽位,需要迁移

迁移槽位

使用 redis-cli --cluster reshard 交互式迁移,或通过命令指定:

1
2
3
4
5
docker exec redis-7000 redis-cli --cluster reshard redis-7006:7006 \
--cluster-from redis-7000:7000,redis-7001:7001,redis-7002:7002 \
--cluster-to redis-7006:7006 \
--cluster-slots 1000 \
--cluster-yes

该命令从现有 3 个 master 各迁移一部分槽位,总计 1000 个槽到新节点。

查看迁移后的槽位分布:

1
2
docker exec redis-7000 redis-cli -p 7000 cluster nodes
# 新节点拥有 1000 个槽

缩容:先将要下线节点的槽位迁移到其他节点,然后从集群中移除节点。具体命令类似,这里不再展开。

验证信息:扩容和槽位迁移在 Redis 7.2 环境验证,迁移过程中集群读写正常,客户端会自动处理 ASK 重定向。

4. 容量规划与关键监控指标

在生产部署前,必须做好容量规划,否则上线后再扩容可能造成性能抖动。

4.1 容量估算方法

内存估算

  • 单条数据平均大小:例如用户会话对象序列化后 2KB。
  • 总 key 数量:例如 1 亿用户。
  • 主从复制冗余:每个 master 需要分配约 1.5~2 倍内存用于 slave 和内存碎片。
  • 集群分片数:总内存 / 单节点内存上限。生产建议单节点内存不超过 32GB(避免大 RDB 恢复慢)。

示例计算

总数据量:1 亿 * 2KB = 200GB
加上 Redis 内部开销(约 20%):240GB
若使用 3 主 3 从,每个 master