Redis 高可用:主从、哨兵、Cluster 三件套,一台机器挂了怎么不停服务?

0 阅读10分钟

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 个奇数个,防哨兵自己单点),它们的工作分四步:

  1. 监控:定时 ping 主节点和从节点,看它们活着没。
  2. 主观下线(sdown):某个哨兵连续一段时间 ping 不通主——它自己单方面觉得主挂了。但这只是一个保安说"大门坏了",还不作数。
  3. 客观下线(odown):它去问另外几个哨兵"你们那边主还活着吗?"如果**达到法定人数(quorum,比如 2 个)**的哨兵都说主挂了,这才算"真的挂了"。
  4. 选举 + 故障转移:哨兵们投票选出一个"领班哨兵",由它挑一个数据最新的从节点提拔成新主,再让其它从节点改去跟着新主。全程自动。

脑裂(面试常问,讲人话):主节点"假死"(比如网络堵了,其实它还活着还在写)。哨兵们误以为它挂了,提拔了一个新主。这时候旧主突然活了,还在接收业务写请求——于是变成两个主同时在写,数据分叉了。缓解办法之一:给主配 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→槽
}

六、挑战题(答案都在仓库里,跑起来才知道)

  1. ⭐ 在主从拓扑里往主写一个新 key,到两个从分别 get 看能否读到;再把一台从停掉,观察主 info replication 里 connected_slaves 的变化。
  2. ⭐⭐ 手动再做一次故障转移:kill 当前主节点,用 docker logs redis-sentinel-1 找出 +switch-master 那一行,确认新主是谁,再去新主上 info replication 看它的从列表。
  3. ⭐⭐ 在集群里用 redis-cli -c -p 17000 set 和不带 -c 各写同一个 key,对比一次返回 OK、一次返回 MOVED——想想为什么,JedisCluster 又是怎么帮你免掉这件事的。
  4. ⭐⭐⭐ 思考题:如果集群里某个主节点宕机(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 倍差距。

跑完上面任何一步遇到问题,把终端输出发评论区,一起排查。