老王经营着一家叫“Redis“的烘焙连锁店。总店(Master)负责研发新配方、掌握核心数据,分店(Slave)负责照方烤面包、服务更多顾客。为了让分店能做出和总店一模一样的产品,老王每天都要把配方同步过去。可问题来了:总店偶尔会停电、断网,如果没人盯着,顾客来了只能空手而归。于是老王雇了一支督导团队(Sentinel)——他们定时给各店发”在吗“的消息,一旦确认总店出了大事,就立刻从分店中选一家升格为新总店,让生意不打烊。这套”配方同步 + 督导巡查 + 紧急升格”的体系,就是 Redis 主从复制与哨兵高可用要讲的故事。
对后端和缓存架构工程师来说,主从复制与哨兵是 Redis 生产环境的“标配安全带”:既能横向扩展读能力,又能在主节点宕机时自动恢复服务,是搭建高可用缓存层的必备技能。
目录
可视化演示
通过主从复制数据同步流程、哨兵集群监控与故障检测、以及自动故障转移Leader选举三个维度,展示Redis高可用架构的完整机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Redis主从复制机制 全量同步通过RDB快照传输完整数据到从节点 增量同步通过命令传播实时复制主节点写入 Sync=Full(RDB)+Incr(Cmd) 全量+增量两阶段保证数据一致性
主从架构:总店与分店的分工
老王开了家烘焙店,生意好到顾客排队到街角。他决定开两家分店,但有个规矩:配方只有总店能改,分店只负责照着配方做面包。顾客想买新口味的面包,走几步路去分店就行,不用再挤总店。这就是主从复制的朴素版本:一个主节点(Master)负责接收写请求,多个从节点(Slave)负责复制数据并提供读服务。
在 Redis 里,主从复制就是让一个节点当“主”,其余节点当“从”。主节点接收客户端的所有写命令,从节点通过复制主节点的命令流让自己的数据和主节点保持一致。于是数据有了多份副本,一台机器坏了数据还在——这就是“数据冗余”;写走主、读走从——这就是“读写分离”。配置从节点很简单,执行 REPLICAOF 主节点IP 端口 或启动时加 --replicaof 即可。拓扑上最常见的是“一主多从”,还可以做“链式复制”——从节点下面再挂从节点,减轻主节点的分发压力。
从业务视角看,读写分离的收益可以量化:写请求只走主,读请求可以分流到所有从节点,系统的读吞吐量随从节点数量线性增长:
回到烘焙店:配方本只有一本,但每个分店都能按配方烤面包,分店越多,能同时招待的客人就越多。唯一要注意的是,面包口味始终由总店决定,分店绝不能自己研发新配方——在 Redis 中,从节点默认只读,写入会被拒绝。
- QPS 读整个主从集群每秒能处理的读请求数量,是衡量读能力的关键指标。
- QPS 主读主节点自身承担的读请求量,通常是少量管理类查询或无法分流的关键读。
- 求和符号把从 1 号到 n 号每个从节点的读 QPS 累加,n 越大读能力越强。
全量同步:第一次大搬家
第一家分店开张时,总店已有三年积累的配方。老王把整套配方装订成一本厚厚的《配方全集》,派店员整本送过去。但问题来了:装订这本书花了两小时,这两小时里总店又卖出了十几个新品种,这些没写进书里怎么办?老王让助手拿一本“备忘本”,把新配方一条条记下来,等分店收到《配方全集》,再把备忘本里的内容补传过去。
这就是 Redis 全量同步(Full Resynchronization)的完整过程。从节点第一次连接主节点,或者断线太久已经无法增量续传时,主节点会 fork 出一个子进程生成 RDB 快照;与此同时主节点照常处理写命令,并把新命令写入复制缓冲区(repl_backlog)。快照传完后,从节点把 RDB 加载进内存;紧接着主节点把缓冲区里积压的命令继续发给从节点,从节点再执行一遍,最终追上主节点的进度。
全量同步的总耗时可以拆成三段:
全量同步就像分店开张的第一课:把压箱底的配方整本搬过去。虽然搬运重,但只此一遭,之后配方同步就不再这么大动干戈了。
- T 生成主节点 fork 子进程扫描内存生成 RDB 文件所需时间,期间主节点仍可正常服务客户端。
- T 传输RDB 文件从主节点通过网络送到从节点的时间,受带宽和文件大小影响。
- T 加载从节点收到 RDB 后逐条解析并写回内存的时间,期间从节点还不能提供服务。
增量同步:对账补页
分店运营进入正轨后,老王发现每天要同步的新配方其实就几页。如果每天都重新搬一整本配方全集,太浪费了。于是老王规定:总店和分店各自记一个“配方版本号”——在 Redis 中叫复制偏移量(offset)。每次同步前先对账,缺哪几页就补哪几页,这就是增量同步(Partial Resynchronization)。
Redis 的实现是:主节点维护一个环形的复制积压缓冲区(repl_backlog),最近执行的写命令都留在这里;主从节点各自维护自己的复制偏移量。从节点断线重连后,把自己的偏移量告诉主节点,主节点一算:你想追的位置还落在我的积压缓冲区里,好办,直接从缓冲区把后续命令发给你。可如果从节点落后太多,缓冲区早被新命令覆盖了,那就只能退回全量同步。
判断能否增量同步的条件可以写成:
增量同步像分店日常对账:总店把改动的那几页配方传真过去,分店誊抄一遍,两边版本号对齐,皆大欢喜。
- slave_offset从节点当前已复制的命令位置,相当于它已经誊抄到配方的第几页。
- master_offset主节点当前最新的命令位置,相当于总店已经写到了第几页。
- backlog_size复制积压缓冲区的容量,相当于总店愿意保留最近多少页的改稿。
- 不等式含义只要 slave 的位置没有掉出缓冲区保留范围,就可以从缓冲区里补发后续命令;否则只能整本重搬。
哨兵监控:督导的巡查
老王事业越来越大,不可能天天盯着每家分店。于是他雇了几名督导——也就是哨兵(Sentinel)。每名督导每隔几秒给总店和分店发一条“在吗?”(PING),收到“在的”(PONG)就放心了。如果连发几条都没回应,督导会在值班日志上记一笔“疑似失联”——这就是主观下线(sdown,subjectively down)。注意,单个督导的判断不一定准,可能只是网络抖动。
所以老王定下规矩:一名督导说失联不算数,必须有多名督导同时说“这家店失联了”,才算“确认失联”——这就是客观下线(odown,objectively down)。这个“多名”的阈值叫 quorum。Redis 哨兵监控的核心就是这套“先怀疑、再确认”的多数派判定逻辑:先由单个哨兵判定 sdown,再由多个哨兵共同确认 odown,最终触发故障转移。
客观下线的判定公式是:
督导的巡查不是一次定生死,而是“一个人怀疑,一群人确认”——这套机制就是为了避免网络抖动造成的误判误切。
- count(sdown)统计所有哨兵中给主节点打了“疑似失联”标签的数量。
- quorum配置文件设定的阈值,例如 2,表示至少 2 名哨兵都判失联才可信。
- 不等号判定人数达到或超过阈值时,哨兵集体认定主节点客观下线,准备切换。
故障转移:紧急升格新总店
总店被确认失联后,督导们要开会选一家分店来当新总店。选谁呢?老王给每家分店定了个“优先级”,数字小的优先升格;如果优先级相同,就看谁手里的配方最全,也就是复制偏移量最大;如果还相同,就比谁的编号靠前,即 RUNID 字典序小者优先。选定新总店后,督导团做三件事:通知其他分店改向新总店申请复制、把“新总店是谁”广播给所有客户端、更新自己的本地配置。这套动作就是自动故障转移(Failover)。
Redis 哨兵的选主规则可以概括为一条排序链:
故障转移让客户几乎无感:总店虽然倒了,督导团已经挂好新总店的门牌,食客们继续买面包,只有店员们知道刚刚经历了一场紧急交接。
- priority 升序从节点的 replica-priority 配置,值越小越有资格成为新主。
- offset 降序复制偏移量越大,说明从节点数据越接近旧主,切换后丢失的数据越少。
- runid 升序每个 Redis 实例都有随机 RUNID,优先级和偏移量相同时用它做“抽签”,保证排序唯一。
哨兵集群:督导团协作
如果一个连锁集团只雇一名督导,这名督导自己休假或生病时,再赶上总店失联,就没人处理了。所以老王组建了督导团。督导团成员之间通过 Redis 的发布订阅(Pub/Sub)通道互相认识、交换信息。遇到重大决定——比如执行故障转移——不能由单个督导拍板,必须由督导团推选一位“团长”(Leader)来负责执行,其他人监督。
哨兵集群天然要求“多数派原则”:假如有 3 个哨兵,其中一个在分区里联系不上另外两个,它不能自作主张去切换主节点。只有在获得足够票数后,候选哨兵才能拿到故障转移的执行权。这个票数门槛既不能低于配置的 quorum,也不能低于哨兵总数的一半加一:
督导团的存在,让“监控者”自己也成为被监控对象。大家互相盯着、彼此制衡,整个系统就不会因为“某个人走神”而崩溃。
- max(quorum, 多数)在配置阈值和多数派阈值之间取大者,防止极少数哨兵在分区中擅自切换。
- n_sentinel哨兵集群中参与投票的哨兵总数。
- 多数派阈值3 个哨兵的多数派是 2,5 个哨兵是 3,保证切换决定代表大多数。
脑裂与优化:失联总店的乌龙
最让人哭笑不得的事故是:总店并没有真着火,只是门前的电话线被施工队挖断了。督导团联系不上总店,按流程选了新总店。可老总店并不知道这一切,还在正常接单、正常烤面包——顾客下订单它照收,配方改动它照记。等电话线修好,老总店发现自己已经被“革职”,这段时间独自记下的改动全部作废。这个局面就是“脑裂”(Split-Brain):网络分区把原本的一个系统撕成了两个各自主张的部分。
Redis 用两个参数来扎紧篱笆:min-replicas-to-write 和 min-replicas-max-lag。前者要求主节点只有在至少还有 N 个从节点在线的情况下才允许写入;后者要求这些从节点的最大同步延迟不超过 M 秒。一旦网络分区导致老主节点和所有从节点断开,它就会触发“停止写入”的保护,从而大幅减少脑裂期间产生的孤儿数据:
回到烘焙店:总店一旦发现自己和所有分店失联,就要主动挂出“暂停接单”的牌子,而不是闷头烤面包。虽然会损失一点生意,但至少不会在恢复通信后面对一屋子卖不出去的面包。
- #replicas主节点视角中仍然保持连接和通信的从节点数量。
- min_replicas_to_write允许写入所需的最小从节点数,例如 1,达不到就拒绝写入。
- max(lag)所有在线从节点中最大的同步延迟秒数。
- min_replicas_max_lag允许的最大延迟阈值,例如 10 秒,延迟太高的集群宁可拒写。
AI 应用场景
AI 推荐系统的特征缓存高可用
阶段:模型在线推理前的特征数据存取阶段
解决的问题:推荐模型每秒要读取数百万个用户特征和物品特征,特征全部缓存在 Redis 中。传统单机 Redis 一旦宕机,整个推荐链路直接熔断,用户刷不出内容。采用 Redis 主从 + 哨兵后,从节点分摊读流量,哨兵在主节点故障时自动切换,特征层可用率从 99% 提升到 99.99% 以上。
具体案例:某短视频推荐系统将用户画像与物品特征写入 Redis,采用一主两从三哨兵架构。主节点承接特征更新写入,两个从节点承接每秒约 8 万次的读请求。某天凌晨主节点所在物理机硬件故障,哨兵在约 10 秒内完成客观下线判定、选主与切换,推荐服务无感知恢复。
大模型推理结果缓存
阶段:大模型推理结果复用与缓存阶段
解决的问题:企业私有化部署大模型时,相似问题反复调用推理会带来高昂的 GPU 成本和响应延迟,需要把高频问答结果缓存到 Redis。缓存层自身必须高可用,否则缓存一挂,流量全部回源打穿 GPU。主从复制提供读扩展,哨兵保障缓存层故障自愈。
具体案例:某企业知识库助手的“如何申请发票”等高频率问题答案缓存在 Redis。主节点负责写入新答案,从节点承担查询流量;当主节点所在容器异常重启时,哨兵快速切换,缓存命中率维持在 90% 以上,GPU 推理调用量不因缓存故障而暴涨。
Python 演示
# -*- coding: utf-8 -*-"""用 Python 模拟 Redis 主从复制同步与哨兵故障转移的核心流程"""import hashlib class RedisNode: """模拟 Redis 节点:维护数据、复制偏移量和写命令日志""" def __init__(self, name, priority=1): self.name = name self.priority = priority self.runid = hashlib.md5(name.encode()).hexdigest()[:6] self.data = {} self.backlog = [] self.offset = 0 self.alive = True def write(self, key, value): if not self.alive: raise RuntimeError(self.name + " is down") self.data[key] = value self.backlog.append((key, value)) self.offset += 1 def ping(self): return self.alive def full_sync(master, slave): """全量同步:把主节点数据整体拷贝给从节点""" slave.data = dict(master.data) slave.offset = master.offset print("全量同步完成: %s -> %s, 数据 %d 条" % (master.name, slave.name, len(slave.data))) def incremental_sync(master, slave): """增量同步:从 backlog 中补发从节点落后部分的写命令""" missed = master.backlog[slave.offset:] for key, value in missed: slave.data[key] = value slave.offset = master.offset print("增量同步: %s 补发 %d 条命令, offset -> %d" % (slave.name, len(missed), slave.offset)) def elect_new_master(candidates): """哨兵选主:priority 小优先, offset 大优先, runid 小优先""" return min(candidates, key=lambda n: (n.priority, -n.offset, n.runid)) def demo(): master = RedisNode("master", priority=1) slave_a = RedisNode("slave-a", priority=1) slave_b = RedisNode("slave-b", priority=2) master.write("user:1", "alice") master.write("user:2", "bob") full_sync(master, slave_a) full_sync(master, slave_b) master.write("order:100", "item-42") master.write("order:101", "item-99") incremental_sync(master, slave_a) incremental_sync(master, slave_b) print("主从数据一致:", master.data == slave_a.data == slave_b.data) master.alive = False sentinel_names = ["sentinel-1", "sentinel-2", "sentinel-3"] sdown = sum(1 for _ in sentinel_names if not master.ping()) quorum = 2 print("哨兵判定: sdown=%d, quorum=%d -> 客观下线=%s" % (sdown, quorum, sdown >= quorum)) new_master = elect_new_master([slave_a, slave_b]) print("选出新主节点:", new_master.name) if __name__ == "__main__": demo()
slave.data = dict(master.data)模拟主节点把 RDB 快照整体拷贝给从节点,是“全量同步”的抽象表达。self.backlog.append((key, value))每写一条命令都追加到日志中,等价于 Redis 的复制积压缓冲区 repl_backlog。missed = master.backlog[slave.offset:]根据从节点偏移量切出缺失命令,模拟增量同步只补发落后部分。key=lambda n: (n.priority, -n.offset, n.runid)用元组排序实现哨兵选主三规则:优先级小、偏移量大、RUNID 小。sdown = sum(1 for _ in sentinel_names if not master.ping())模拟多个哨兵分别探测主节点,统计主观下线的人数。
常见误区
误区“我往从节点写数据,再从从节点同步回主节点,不就能分担主节点的写压力了吗?”
正解从节点默认是只读的(replica-read-only yes)。往从节点写入会被拒绝,即使侥幸写进去,也只会留在那台节点上,永远不会同步给主节点,反而造成主从数据不一致。就像分店厨师私自改了配方,总店和其他分店完全不知情,做出来的面包味道千奇百怪。
为何常见:习惯了 MySQL 主从双写或半同步的开发者,容易把 Redis 的复制模型等同于通用数据库,忽略了 Redis 从节点是“只读副本”的设计。
误区“哨兵就是帮我看门的人,一个就够了,多配几个浪费资源。”
正解如果只有一个哨兵,它本身就是新的单点。它宕机或所处网络被隔离,就没人监控主节点了。而且客观下线依赖多个哨兵投票,只有一名哨兵时,它的判断就是最终判断,一旦误判就会错误切换。生产环境至少部署 3 个哨兵(奇数个),像督导团一样互相看护。
为何常见:只看到哨兵在监控主从节点,忽略了“监控者自身也需要高可用”这一层。
误区“从节点第一次连主节点,主节点发一个 RDB 快照文件给从节点,加载完就完事了。”
正解RDB 生成需要时间,这期间主节点仍在正常服务,新写命令源源不断进来。只传 RDB 会让从节点缺了生成期间的数据。所以主节点会把生成 RDB 期间的写命令缓存到复制缓冲区,传完快照后再补发。就像先给分店整本老配方,再把印刷期间的新配方手工补上。
为何常见:把“快照”想象成静态拷贝,忽略了 Redis 是持续对外服务、数据时刻在变化的事实。
误区“主节点坏了不是一瞬间就能换吗?我看文档里写着哨兵能自动故障转移。”
正解一次完整故障转移要经历超时等待、主观下线、多个哨兵确认客观下线、Leader 选举、选新主、通知其他从节点、广播给客户端等多个环节,整体耗时通常在几秒到十几秒。如果业务对中断零容忍,还需要客户端配合做本地降级或写缓冲。
为何常见:把“自动”理解为“瞬间”,没有把故障检测的完整链路放进心里,对故障转移的耗时没有预期。
误区“数据都多副本了,还有哨兵看着,不可能丢数据吧?”
正解主从复制是异步的,主节点在自己最新的写命令尚未同步给从节点时宕机,这条命令就丢了。脑裂场景更危险:老主在网络分区期间写入的数据,在新主上任后全部无法同步,等同于丢失。min-replicas-to-write、AOF 持久化可以减少丢失窗口,但无法做到零丢失,这是以可用性换一致性的权衡。
为何常见:混淆了“高可用”与“数据零丢失”,把复制冗余等同于持久化保证。
面试题
Q1. Redis 主从复制中,从节点默认处于什么状态?
- A.可读可写
- B.只读不可写← 正确答案
- C.只写不可读
- D.读写都不可
答案:B。从节点默认 replica-read-only yes,只可读不可写。分店只能照着配方烤面包,不能自己改配方。A 错在从节点不可写,C 错在从节点可读,D 错在从节点具备读能力。
Q2. 关于 Redis 主从复制的作用,下列说法错误的是?
- A.实现数据冗余备份
- B.实现读写分离扩展
- C.可以自动故障转移
- D.提升写请求并发量← 正确答案
答案:D。主从复制让从节点分担读流量,写请求仍集中在主节点,因此不能提升写并发量,D 错误。数据冗余、读写分离是主从复制的基本收益;“自动故障转移”是配合哨兵后获得的能力。
Q3. 从节点第一次连接主节点时,Redis 会执行哪种同步?
- A.增量同步
- B.全量同步← 正确答案
- C.命令传播
- D.哨兵选举
答案:B。新从节点没有复制偏移量,无法和主节点的积压缓冲区对齐,必须先做一次全量同步拿到基线数据。就像新分店没有配方底子,只能整本搬配方,不能只补几页。
Q4. 主节点上的复制积压缓冲区(repl_backlog)最主要的作用是?
- A.持久化全部写入数据
- B.记录快照生成的时刻
- C.缓存最近的写命令← 正确答案
- D.暂存来自从库请求
答案:C。repl_backlog 是一个环形缓冲区,保存最近执行的写命令,供断线重连的从节点增量补发。它不负责持久化所有数据,也不处理从库的读请求。它更像是总店保留的最近几页改稿,够从节点对账补页就行。
Q5. 关于哨兵的“主观下线”和“客观下线”,下列说法正确的是?
- A.单个哨兵判定即为主观下线← 正确答案
- B.多个哨兵判定才为客观下线← 正确答案
- C.客观下线需要达到 quorum← 正确答案
- D.主观下线就立刻切换主节点
答案:ABC。主观下线是单个哨兵自己的判断,客观下线需要多个哨兵投票达到 quorum 后才成立。只有客观下线才会触发故障转移,所以 D 错误。整个流程就像督导先记“疑似失联”,等多人确认后才宣布“彻底失联”。
Q6. 哨兵选出新主节点时,以下哪一项不是其参考依据?
- A.从节点的优先级
- B.从节点的复制偏移量
- C.从节点的内存大小← 正确答案
- D.从节点的运行 ID
答案:C。选主依据依次是 replica-priority 数值小者优先、复制偏移量大者优先、RUNID 字典序小者优先。内存大小不属于选主标准。督导选新总店看的是配方全不全、优先级高低,而不是店面规模。
Q7. 为降低脑裂导致的数据丢失,可以采取的措施包括?
- A.设置从节点数下限← 正确答案
- B.设置从节点延迟上限← 正确答案
- C.开启 AOF 持久写盘← 正确答案
- D.升级更大的内存条
答案:ABC。min-replicas-to-write(从节点数下限)和 min-replicas-max-lag(延迟上限)让老主在失去多数从节点时拒绝写入,是防脑裂的核心配置;开启 AOF 能减少故障恢复时的数据丢失。内存大小与脑裂无关。
Q8. 哨兵执行故障转移前,需要获得的授权票数条件是?
- A.一票即可执行
- B.达到 quorum 值
- C.全体哨兵同意
- D.达到多数与 quorum 的较大值← 正确答案
答案:D。Redis 要求授权票数同时满足“不少于 quorum”和“不少于哨兵总数一半加一”,两者取较大值。防止在分区情况下少量哨兵擅自切换,也防止 quorum 配置过低导致误切。督导团要选团长,既要有内定门槛,也得过半数。
Q9. 从节点断线后重新连接主节点,主节点会怎么处理?
- A.总是发全量 RDB
- B.判断后再定同步方式← 正确答案
- C.要求从节点重启
- D.忽略并断开连接
答案:B。主节点会比较从节点上报的偏移量与积压缓冲区范围,如果落后部分仍在 backlog 内就增量补发,否则退回全量同步。所以不是每次都全量,也不是忽略,而是“先对账,再决定怎么补”。
Q10. 关于哨兵集群部署,以下说法正确的有?
- A.哨兵节点数量通常为奇数← 正确答案
- B.三个哨兵可组建最小集群← 正确答案
- C.哨兵之间可互相通信← 正确答案
- D.哨兵与主从共用端口
答案:ABC。哨兵集群通常部署 3 个或 5 个奇数节点,保证多数派容易达成;3 个哨兵就足以组成最小的高可用监控集群;哨兵之间通过 Pub/Sub 通道通信。哨兵默认端口 26379,与 Redis 数据节点端口 6379 不同,所以 D 错误。