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 docker exec redis-master redis-cli set user:1001 '{"name":"Alice"}' docker exec redis-slave1 redis-cli get user:1001 docker exec redis-slave1 redis-cli set test 1
验证信息 :上述命令在 Docker 24 + Redis 7.2 环境验证通过。info replication 可看到 slave0 和 slave1 的 IP 与 offset,说明复制正常。
1.3 主从复制的局限 主从复制解决了数据冗余和读扩展,但存在两个致命问题:
故障无法自动切换 :master 宕机后,需要手动将一个 slave 提升为 master,并修改客户端配置,恢复时间可能长达数分钟。
写能力无法扩展 :所有写请求仍然集中在 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 docker exec sentinel1 redis-cli -p 26379 sentinel master mymaster docker exec sentinel1 redis-cli -p 26379 sentinel slaves mymaster
故障转移演练 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 docker stop redis-master sleep 8docker exec sentinel1 redis-cli -p 26379 sentinel get-master-addr-by-name mymaster docker exec redis-slave1 redis-cli set after-failover 'yes' docker start redis-master docker exec redis-master redis-cli info replication
验证信息 :以上演练在 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" 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
查看集群状态:
1 2 3 4 5 docker exec redis-7000 redis-cli -p 7000 cluster info docker exec redis-7000 redis-cli -p 7000 cluster nodes
验证数据分片:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 for i in $(seq 1 10); do docker exec redis-7000 redis-cli -p 7000 set key:$i value:$i done docker exec redis-7000 redis-cli -p 7000 cluster keyslot key:1 docker exec redis-7000 redis-cli -p 7000 cluster keyslot key:2 docker exec redis-7000 redis-cli -p 7000 get key:3 docker exec redis-7000 redis-cli -c -p 7000 get key:3
故障转移演练 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 docker exec redis-7000 redis-cli -p 7000 cluster nodes | grep master docker stop redis-7000 sleep 8docker exec redis-7001 redis-cli -p 7001 cluster nodes 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 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
缩容 :先将要下线节点的槽位迁移到其他节点,然后从集群中移除节点。具体命令类似,这里不再展开。
验证信息 :扩容和槽位迁移在 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