Redis 高可用:主从、哨兵、Cluster 三件套,一台机器挂了怎么不停服务?
作者:鱼宵 | 实战驱动系列 · 第 4 篇
完整课程与可运行源码已开源在 Gitee:gitee.com/j67mk2/redi… (10 课实战教程,本课源码在 lesson-07/)
一、真实场景:单机 Redis 的两个硬伤
你搭了个 Redis,扛着全站的缓存,跑得好好的。直到有一天:
- 场景 A:服务器宕机了。Redis 里的数据全没了,所有请求瞬间压到数据库——数据库跟着挂,全站雪崩。
- 场景 B:业务起来了,数据量超过这台机器的内存。Redis 装不下了,你总不能天天加内存条吧?
这就是单机 Redis 的两个硬伤:单点故障(挂了就没了)和容量上限(一台机器装不下)。高可用三件套就是来治这两个病的:
| 方案 | 解决什么 | 类比 |
|---|---|---|
| 主从复制 | 数据备份、读写分离 | 一本作业本复印好几份放不同抽屉 |
| 哨兵 Sentinel | 主挂了自动发现、自动换主 | 小区保安盯着大门,门坏了自动换锁 |
| Cluster | 数据分片、突破单机容量 | 一个太大的仓库拆成 6 间房分头管 |
演进路线一句话:主从复制(有备份)→ 哨兵(自动换主)→ Cluster(数据分片+大容量)。下面逐个讲。
二、主从复制:先保证"数据有备份"
单份数据放一个 Redis 上,机器炸了数据就没了。于是搞一主(master)多从(replica):主负责写,从实时同步主的数据、负责读。好处两个:容灾(主挂了从还在)+ 读写分离(读压力分摊到多个从)。
同步过程是面试常问的,讲人话分两步:
- 第一次连接:全量同步。从节点刚连上主节点,彼此没有任何共同记忆——主节点把自己当前所有数据"拍一张快照"(RDB)发给从,从照着重建一遍。类比:新员工入职,老板把整本手册复印给他。
- 之后的日常:增量同步(命令传播)。全量同步完成后,主每执行一条写命令,就把这条命令"复读"给所有从,从跟着执行。类比:员工入职后,老板每改一条规定都微信同步给大家。
配置只要一句:从节点上加 replicaof <主IP> <主端口>,主节点啥都不用配。
一个必须知道的坑:复制是异步的——主写完就回客户端 OK,不等从复制完。所以极端情况下主刚写完就挂、从还没收到这条命令,会丢这条数据(主从延迟)。这是主从复制的天然短板,哨兵也解决不了,后面讲。
三、哨兵:自动盯梢 + 自动换主
主从复制解决了"有备份",但没解决"主挂了谁来宣布新主"。总不能主一挂,你凌晨爬起来手动登录服务器把从提拔成主吧?于是有了哨兵(Sentinel)。
哨兵是一群独立运行的进程(一般 3 个奇数个,防哨兵自己单点),它们的工作分四步:
- 监控:定时 ping 主节点和从节点,看它们活着没。
- 主观下线(sdown):某个哨兵连续一段时间 ping 不通主——它自己单方面觉得主挂了。但这只是一个保安说"大门坏了",还不作数。
- 客观下线(odown):它去问另外几个哨兵"你们那边主还活着吗?"如果**达到法定人数(quorum,比如 2 个)**的哨兵都说主挂了,这才算"真的挂了"。
- 选举 + 故障转移:哨兵们投票选出一个"领班哨兵",由它挑一个数据最新的从节点提拔成新主,再让其它从节点改去跟着新主。全程自动。
脑裂(面试常问,讲人话):主节点"假死"(比如网络堵了,其实它还活着还在写)。哨兵们误以为它挂了,提拔了一个新主。这时候旧主突然活了,还在接收业务写请求——于是变成两个主同时在写,数据分叉了。缓解办法之一:给主配 min-replicas-to-write 1——主必须至少能跟 N 个从同步上才允许写,网络一断、主连不上从,它就拒绝写,避免脑裂期间脏写。
四、Cluster:数据分片,突破单机容量
主从+哨兵解决了高可用,但数据还是全挤在一个主上,单机内存到头了就没辙。Cluster 把数据分片。
16384 个哈希槽(hash slot):Redis 不是按"哪个 key 放哪台机器"分片,而是先把整个 keyspace 切成 16384 个槽。每个 key 算一个 CRC16 校验值再对 16384 取模,得到它属于几号槽;然后把这 16384 个槽分给多个主节点(比如 3 个主各分约 5461 个槽)。哪个 key 归哪个主,看槽落在谁头上。
MOVED 重定向:你随便连一个节点写 key,它一算"这个 key 的槽不归我管",就回一句 MOVED <槽号> <正确节点地址>,让你跳过去。聪明的客户端(JedisCluster)会记住这张"槽→节点"路由表,以后直接去对的节点,不再绕路。
为什么偏偏是 16384? 答案藏在心跳包里:这是"节点间心跳包大小"和"槽数量够不够分"的权衡。每个节点心跳要捎带自己知道的槽位图,16384 位 = 2KB;如果弄成 65536 槽就变 8KB,心跳太费带宽。而 16384 已经足够日常把数据分散到几十上百个节点了。
哨兵和 Cluster 的区别,面试必背:
| 维度 | 主从 + 哨兵 | Redis Cluster |
|---|---|---|
| 数据分片 | 不分片,全在一个主 | 分片到多个主,每主管一部分槽 |
| 容量 | 受单机内存限制 | 可水平扩容 |
| 客户端 | 连一个地址即可 | 要支持集群协议(JedisCluster),处理 MOVED |
| 故障转移 | 哨兵自动换主 | 节点间自动故障转移 |
| 适合 | 数据量不大、只要高可用 | 数据量大、要水平扩展 |
五、动手验证:亲手 kill 掉主节点,看哨兵自动换主
完整工程在仓库
lesson-07/(含 docker-compose.yml 一键起集群)。下面是本机真实运行输出,全系列最震撼的一段——docker kill redis-master之后,哨兵日志完整记录了换主全过程:
# +sdown master mymaster 172.19.0.2 6379 ← 这个哨兵单方面判定主挂了(主观下线)
# +odown master mymaster 172.19.0.2 6379 #quorum 2/2 ← 凑够 2 票,客观下线
# +try-failover master mymaster 172.19.0.2 6379 ← 开始故障转移
# +vote-for-leader 8e88... 1 ← 哨兵间投票选领班
# +elected-leader master mymaster ... ← 领班选出
# +selected-slave slave 172.19.0.4:6379 ... ← 选中 172.19.0.4 当新主候选
# +failover-state-send-slaveof-noone slave 172.19.0.4 ← 让它"别再跟从任何人"
# +promoted-slave slave 172.19.0.4:6379 ← 正式提拔为新主
# +failover-state-reconf-slaves master ... ← 让其它从改跟着新主
# +switch-master mymaster 172.19.0.2 6379 172.19.0.4 6379 ← 切换完成!
# +failover-end master mymaster 172.19.0.2 6379 ← 转移结束
看懂这条链,哨兵机制你就彻底通了:单方面觉得挂(sdown)→ 投票凑够数(odown)→ 选领班 → 挑新主 → 切换(switch-master)。跑一遍比背十遍都管用。
Cluster 的 MOVED 也实测给你看。key lesson07:greeting 算出来是槽 13384,归 17002 节点管,我们故意连只管 0-5460 槽的 17000 去写它:
=== key 'lesson07:greeting' 落在哪个槽 ===
13384
=== 直接在 17000 上写它 ===
MOVED 13384 127.0.0.1:17002 ← 17000 说:这key不归我,你去 17002
=== 用 -c 集群模式写(自动跳转)===
OK
数据确实分片了——写 9 个 key,散落在三个主上(槽 3867、7994、12121…各归各的节点)。JedisCluster 代码里你完全不用关心 key 在哪个节点,它替你算槽、跳转、刷新路由:
Set<HostAndPort> seedNodes = new HashSet<>(); // 种子节点:给集群一小部分地址即可
seedNodes.add(new HostAndPort("127.0.0.1", 17000));
seedNodes.add(new HostAndPort("127.0.0.1", 17001));
seedNodes.add(new HostAndPort("127.0.0.1", 17002));
try (JedisCluster cluster = new JedisCluster(seedNodes)) {
cluster.set("lesson07:greeting", "你好,Redis Cluster!"); // 自动算槽、自动路由
String v = cluster.get("lesson07:greeting"); // 读回来,自动去对的节点
int slot = JedisClusterCRC16.getSlot("lesson07:greeting"); // 直观感受 key→槽
}
六、挑战题(答案都在仓库里,跑起来才知道)
- ⭐ 在主从拓扑里往主写一个新 key,到两个从分别
get看能否读到;再把一台从停掉,观察主info replication里connected_slaves的变化。 - ⭐⭐ 手动再做一次故障转移:kill 当前主节点,用
docker logs redis-sentinel-1找出+switch-master那一行,确认新主是谁,再去新主上info replication看它的从列表。 - ⭐⭐ 在集群里用
redis-cli -c -p 17000 set和不带-c各写同一个 key,对比一次返回OK、一次返回MOVED——想想为什么,JedisCluster 又是怎么帮你免掉这件事的。 - ⭐⭐⭐ 思考题:如果集群里某个主节点宕机(
docker kill redis-node-1),Cluster 会怎么自动处理?你的 JedisCluster 还能正常读写落在它槽上的 key 吗?(提示:它有从节点。动手 kill 一个主节点观察cluster nodes变化。)
七、面试回答模板(背下来)
面试官:Redis 高可用是怎么做的?
三层演进:主从复制保证数据有备份(第一次全量同步 RDB,之后增量同步命令传播,异步复制有延迟);哨兵负责自动换主(监控 → 主观下线 → 客观下线凑 quorum → 选举领班 + 故障转移,还有脑裂问题用 min-replicas-to-write 缓解);Cluster 做数据分片突破容量(16384 个哈希槽,CRC16 取模定位,MOVED 重定向,客户端 JedisCluster 自动路由)。
选型:数据量不大只要高可用用主从+哨兵;要水平扩容用 Cluster。
八、总结
| 层 | 解决什么 | 关键机制 | 面试高频点 |
|---|---|---|---|
| 主从复制 | 数据备份、读写分离 | 全量(RDB)+增量(命令传播),异步 | 异步会丢刚写的数据 |
| 哨兵 | 主挂了自动换主 | sdown → odown → 选主 → switch-master | 脑裂怎么缓解 |
| Cluster | 突破单机容量 | 16384 槽 + MOVED | 为什么 16384、哨兵 vs Cluster |
记住这条:单机 Redis 的两个病(挂了就没、装不下),分别用主从(备份)和 Cluster(分片)治,哨兵负责中间那个"自动换主"。
九、关于这个系列
本文是「Java 后端实战精通营」系列第 4 篇,原则:实战驱动、由浅到深、面试向,每篇文章的结论都可以亲手验证。
👉 Redis 实战精通营(10 课):gitee.com/j67mk2/redi…
- 本文对应源码位置:
lesson-07/(docker-compose.yml 一键起集群、JedisCluster demo,挑战题 2 的答案就在哨兵日志里) - 前 3 篇已发布:《分布式锁》《缓存一致性》《事务与 Lua》——这套 Redis 系列从单节点一路走到高可用,建议按顺序读
- 后续系列(陆续发布):Dubbo / JVM / JUC / RocketMQ / MySQL / Elasticsearch
下一篇预告:《Redis 为什么这么快?——IO 多路复用、pipeline 提速 100 倍、bigkey 排查实战》——从"高可用"回到"高性能",亲手测出 pipeline 的 108 倍差距。
跑完上面任何一步遇到问题,把终端输出发评论区,一起排查。