Theory · Redis · Replication & Cluster
异步复制 + 自动故障转移 + 数据分片 —— 高可用与水平扩展的三级阶梯
replid + offset 标识数据版本;断线先试 PSYNC 部分重同步,backlog 撑不住才全量 bgsave
sdown → odown(quorum)→ 多数派授权选 leader → 提升从库;本质是 AP,脑裂窗口只能缩小
16384 slot(CRC16 mod 16384)+ gossip 协议 + MOVED/ASK 重定向——分片与高可用一体
Replication Basics
主库把"改变数据集的事件"以命令流发给从库:客户端写、键过期、内存淘汰(都合成 DEL 传播)等——从库是主库写命令的重放器。
网络抖动或超时断开后,从库自动重连并尝试部分重同步:只补断线期间错过的那段命令流(第 4 页)。
backlog 不够或 replid 已换代时退回全量:主库 bgsave 快照 + 传输 + 加载 + 补缓冲(第 3 页)。
| 重要事实(官方文档口径) | 说明 |
|---|---|
| 异步复制 | 主库不等从库确认就回复客户端——低延迟的前提,也是故障转移可能丢已确认写的根源;WAIT 命令可要求 N 个副本确认,但文档明说它"不能把一组实例变成 CP 系统" |
| 双端基本非阻塞 | 主库同步期间照常服务;从库初始同步可继续用旧数据,唯加载 RDB 那一刻短暂阻塞(4.0 起旧数据删除可放后台线程) |
| 从库默认只读 | replica-read-only yes(2.6 起默认);可写副本官方明说不推荐——命令在主从上重放结果可能不同 |
| 级联复制 | 从库可再挂从库(A→B→C);4.0 起子从库收到的是与主库发出的完全相同的流 |
Full Resynchronization
Partial Resync · PSYNC
replid(大伪随机串)标记一段数据历史,offset 按复制流字节单调递增——(replid, offset) 唯一确定数据集的一个版本。主从握手后从库继承主库 replid。
主库在内存里维护固定大小的环形缓冲区(repl-backlog-size),持续滚动写入命令流;offset 落在环里且 replid 匹配 → 回 +CONTINUE 只补增量;环已覆盖/ID 不认识 → +FULLRESYNC。
| 追问 | 答案 |
|---|---|
| backlog 默认多大?够吗? | 默认 1MB(redis.conf 8.2),生产几乎必调大;repl-backlog-ttl 3600——主库长时间无从库连接后 1 小时释放 backlog |
| backlog 怎么估算? | 经验公式:平均主库写速率 × 预期最长断线时间(留余量 ×2)。例:写速率 5MB/s、断线容忍 60s → 至少 300MB |
| 从库重启能部分重同步吗? | 4.0 起、且是正常重启:RDB 里存了 replid/offset(建议先 SHUTDOWN 落盘);AOF 重启不行(没法部分同步 AOF 重启的从库,官方原文) |
| 故障转移后其他从库要全量吗? | 不用。每个实例有主副两个 replid:从库晋升主库时把旧 replid 存进 secondary,老从库拿旧 ID 来连也能 +CONTINUE |
Diskless · Cascading · Expiry · Config
子进程直接把 RDB经 socket 发给从库,不落盘中转——2.8.18 引入;repl-diskless-sync yes(7.0 起默认 yes),repl-diskless-sync-delay 5 秒等待更多从库一起传,摊薄 fork 成本。
A→B→C:中间层从库分担主库的同步压力(多从库读流量也由它分摊)。4.0 起子从库收到与顶层主库相同的流,且从库本地写不会传播(数据一致性靠主库单向流)。
从库不主动删,等主库过期/淘汰时合成 DEL 传播;从库用逻辑时钟对读请求"逻辑隐藏"已过期键。晋升为主库后才开始独立删——主从不依赖时钟同步。
| 参数(redis.conf 8.2) | 默认 | 一句话 |
|---|---|---|
repl-backlog-size | 1mb | 部分重同步的环形缓冲——按写速率×断线时长调 |
client-output-buffer-limit replica | 256mb 64mb 60 | 从库输出缓冲:硬 256MB 或 60s 内超 64MB 即断开 |
repl-timeout | 60s | 主从心跳/传输超时阈值 |
repl-diskless-sync / -delay | yes / 5 | 无盘复制开关与凑批延迟(7.0 起默认 yes) |
replica-priority | 100 | 哨兵选新主的优先级,0 = 永不晋升 |
min-replicas-to-write / min-replicas-max-lag | 0 / 10 | 安全阀:少于 N 个滞后 ≤M 秒的从库时主库拒写(默认关闭,第 10 页) |
Sentinel · Overview
监控:持续 ping 主从是否正常;通知:异常时通过 API/脚本告警;自动故障转移:主挂了 → 提升从库为新主 → 其余从库改挂新主 → 通知客户端新地址;配置提供者:客户端向哨兵问"当前主库是谁"(SENTINEL get-master-addr-by-name)。
至少 3 个哨兵实例,放在"独立失败域"(不同物理机/AZ);哨兵端口 26379;必须给可写配置文件(状态要落盘);少数派分区绝不 failover——哨兵自己也要多数派才能干活。两节点哨兵是经典错误部署。
| sentinel.conf 关键项(默认值) | 说明 |
|---|---|
sentinel monitor <name> <ip> <port> <quorum> | 监控主库与判定客观下线的 quorum(只管"检测",不管"授权"——见下页) |
down-after-milliseconds(默认 30000) | PING 无有效回复持续该时长 → 判 sdown |
failover-timeout(默认 180000) | 故障转移总超时;同一 sentinel 对同一 master 重试要等 2× 该值 |
parallel-syncs(默认 1) | 切换后允许同时重新同步的从库数——全 1 可保证切换期间始终有从库可读 |
__sentinel__:hello 频道(每 2 秒发 hello)互相发现。Docker/NAT 端口映射会破坏自动发现,须 announce-ip/-port。Failure Detection & Leader Election
单个哨兵在 down-after-milliseconds 内没收到对 PING 的有效回复(+PONG / -LOADING / -MASTERDOWN 算有效)→ 打 sdown。sdown 只代表"我觉得它挂了",其他实例也可能 sdown。
哨兵用 SENTINEL is-master-down-by-addr 互相询问,quorum 个哨兵都报 sdown → 升级 odown。只对主库生效(从库/哨兵最多 sdown)。odown 触发故障转移流程,但还不等于能执行。
| 角色 | 门槛 | 作用 |
|---|---|---|
| quorum | 配置值(如 2/5) | 把 sdown 升级为 odown——只负责"确认主库真的挂了",是检测门槛 |
| majority | 过半哨兵(5 个取 3) | 授权某个哨兵当 leader 去执行故障转移——是执行门槛;少数派分区永远拿不到 |
| leader 选举 | epoch(类比 Raft term)+ 先到先得 | 每个哨兵在同一 epoch 只投一票;拿多数票者当选,获唯一 config epoch,新配置版本号大者胜 |
Failover Timeline
Replica Selection & Promotion
与主库断开超过 down-after-milliseconds × 10 + 主库进入 sdown 的时长 的从库直接出局——数据太旧,晋升会放大丢失。sdown 状态的从库也不会被选。
① replica-priority 小者优先(运维显式指定机房偏好,默认 100);② 优先级相同比复制 offset——数据更全的赢;③ 仍相同比 runid 字典序——不为优,只为结果确定性(不用随机)。
| 追问 | 答案 |
|---|---|
| priority=0 的从库会怎样? | 永远不会被晋升(运维用来做"纯读副本/异地灾备从库"),但故障转移后仍会被哨兵重配为从库挂到新主 |
| failover 什么时候算成功? | 对选中从库 REPLICAOF NO ONE 成功执行,且在它的 INFO 里观察到 role 变为 master;随后新配置(带唯一 config epoch)向全体广播 |
| 两个哨兵同时想切换怎么办? | 投过票的哨兵在 2 × failover-timeout 内不再投同一 master 的其他请求——同一时刻只有一个 leader 能推进 |
| 哨兵会误判自己挂了怎么办? | TILT 保护模式:时钟异常(两次定时中断间隔为负或 ≥2s)时只监控不行动,30 秒正常后退出 |
Split Brain · Data Loss Window
① 网络分区把旧主 M1 和客户端 C1 划在少数派一侧;② 多数派哨兵完成 failover,R2 晋升新主;③ C1 不知道,继续往 M1 写,M1 照常接受;④ 分区恢复,M1 被重配为 R2 的从库并清空自身数据——分区期间 C1 写入的数据全部丢失。
min-replicas-to-write 1 + min-replicas-max-lag 10:主库发现没有 1 个从库的 ack 滞后 ≤10 秒,就拒绝写入。旧主被隔离后 10 秒即变为不可用——官方文档推荐配置。默认 0/10 即关闭(redis.conf 8.2)。
| 仍然存在的丢数据窗口 | 说明 |
|---|---|
| ① 拒写前的滞后窗口 | 检测有延迟:拒写生效前的写入(≤min-replicas-max-lag 级别)已接受但未到达从库——切换后照样丢 |
| ② 异步复制的固有窗口 | 主库回复 OK 与传播到从库之间有时间差;主库恰好在这瞬间挂掉、未收到写的从库被晋升 → 该写永久丢失 |
| ③ WAIT 也不解决 | 官方原文:WAIT 只保证"有 N 份 ack","不能把实例组变成 CP 系统"——已确认的写仍可能在故障转移中丢失 |
Cluster · Sharding Model
16384 Rationale · Gossip Protocol
① 心跳包带完整槽位图:每个 PING/PONG 都携带本节点的槽 bitmap——16384 bit = 2KB,65536 bit = 8KB,gossip 里这个包每秒都在飞,×4 的带宽是"prohibitive"(禁止性开销);② 官方建议集群不超过 ~1000 个主节点(全连接 N-1、gossip 流量),16384 个槽给 1000 个节点分绰绰有余;③ CRC16 输出 16 位,只取 14 位(mod 16384)。
全连接 + gossip 传播(消息数不随 N 指数爆炸):正常时每秒随机 ping 少量节点;超 NODE_TIMEOUT/2 没联系过的节点必 ping。
| 消息 | 语义 | 要点 |
|---|---|---|
MEET | 邀请入群(CLUSTER MEET) | 与 PING 相同结构,但强制接收方把新节点当自己人——集群信任的起点,其余靠 gossip 链式传播 |
PING | 心跳探测 + 携带自身配置 + gossip 少量随机节点视图 | 槽位图、configEpoch、节点 flags 都在包头;gossip 部分带几个随机节点状态 |
PONG | PING 的回应 | 也可主动单发(不等 ping)用于尽快广播新配置,如晋升后 mass-broadcast |
FAIL | 节点死亡的全局广播 | PFAIL(本地判定:超过 node-timeout 无 PONG)→ 多数 master 确认 → 广播 FAIL,强制所有节点标记——触发从库选举 |
Redirection & Smart Client
| 重定向 | 含义 | 客户端该做什么 | 语义细节 |
|---|---|---|---|
-MOVED 3999 host:port | 槽 3999 已永久易主 | 去新节点重发本条命令;并更新本地槽表(通常整表重拉 CLUSTER SLOTS/SHARDS) | 错误里带槽号和负责节点;客户端不更新槽表也能工作(每次都被重定向),但会退化成慢路径 |
-ASK 3999 host:port | 槽 3999 迁移中:这条 key 可能已在目标节点 | 只对下一条命令:先向目标节点发 ASKING 再重发;不更新槽表 | "只借宿一次":槽还没搬完,下一条命令的 key 可能还在源节点——必须先试源节点 |
迁移中同一槽的 key 源/目标两处都有:ASK 让客户端"先源后目标"逐 key 兜底;若此时把槽表直接改成目标节点,还在源节点的 key 就会被打错。官方原文:ASK = "send only the next query to the specified node"。
启动拉取槽表 → 本地算槽直连正确节点(正常路径零重定向)→ 收到 MOVED 时刷新槽表并重试 → 定期/事件驱动维护。redis-cli -c 是"每次都重定向"的笨客户端演示;生产用 go-redis/Jedis/lettuce 等 cluster 模式。
Replica Election · Resharding
主库被标 FAIL 后,从库按复制 offset 排名(rank 0 = 数据最新)错峰发起选举:DELAY = 500ms + rand(0~500ms) + rank × 1000ms。向所有 master 请求投票(FAILOVER_AUTH_REQUEST),多数 master 授权 → 晋升并生成更大 configEpoch,广播"last failover wins"。
主库超 cluster-node-timeout(默认 15000ms)联系不上多数 master → 进入错误状态停止接受写——少数派分区最多写 node-timeout 这么久。相关参数:cluster-replica-validity-factor 10(断链过久不参选)、cluster-require-full-coverage yes(槽没全覆盖就拒写)。
| 扩缩容五步(搬槽 8 从 A 到 B) | 说明 |
|---|---|
① B:SETSLOT 8 IMPORTING A | 目标节点标记"正在从 A 收槽" |
② A:SETSLOT 8 MIGRATING B | 源节点标记"正在向 B 迁槽"——此后 A 对该槽不存在的 key 回 ASK 指向 B |
③ A:CLUSTER GETKEYSINSLOT 8 n | 分批取该槽的 key |
④ A → B:MIGRATE ... KEYS k1 k2 | 原子搬运:序列化传输 + 两端短暂锁定 + 删源——任一时刻 key 只在 A 或 B |
⑤ 双方 + 全体:SETSLOT 8 NODE B | 恢复正常态;全体发是为免等 gossip 自然收敛。迁移中跨两节点的多 key 命令返回 -TRYAGAIN |
Hash Tags · Limits · Trade-offs
多 key 命令 / 事务 / Lua 涉及的所有 key 必须同槽(否则 CROSSSLOT 错误)。hash tag 强制同槽:key 含 {...} 时只哈希大括号内第一段内容——{user1000}.following 与 {user1000}.followers 必同槽。规则边角:foo{}{bar} 整 key 哈希({} 内为空);foo{{bar}}zap 哈希的是 "{bar"。
把业务对象绑到同一槽 = 放弃该对象在槽间的负载均衡;热点用户的 tag 会让某槽变成热槽。实践:以"业务聚合单元"(user/order)为 tag 粒度,而不是大客户 ID 直接当 tag。SCAN 类全库遍历需客户端逐槽聚合;KEYS 只在单节点视角生效。
| 维度 | 主从 + 哨兵 | Cluster | 客户端分片(hash 取模/一致哈希) |
|---|---|---|---|
| 容量 / 写扩展 | 单主写,纵向为主 | 多主分片,写近线性扩展(≤1000 节点) | 可扩展,但扩容要大规模搬数据(取模)或依赖预分片 |
| 高可用 | 哨兵自动 failover(多数派授权) | 内置:FAIL 广播 + 从库选举 | 自己搭(每片再挂从库 + 自研/外置切换) |
| 客户端复杂度 | 低:连哨兵问主库地址 | 中:cluster 模式客户端(槽表 + 重定向) | 高:分片逻辑全在业务侧 |
| 多 key / 事务 / Lua | 无限制 | 仅同槽(hash tag 缓解) | 跨实例不可行(需业务自己保证) |
| 运维与成熟度 | 简单,规模小标配 | 官方方案;运维复杂度高(槽/迁移/广播) | -proxy(如 Codis/自研)增加一层故障点 |
| 适用规模 | 数据 < 单机内存、写 QPS 单主可扛 | 数据/写压力超出单机,需要水平扩展 | 历史方案为主;新项目首选官方 Cluster |
Interview QA · 1/2
异步:主库不等从库 ack 就回客户端。已确认的写在 failover 时可能丢(晋升了没收到写的从库)。WAIT 可要求 N 副本 ack,但官方明说它不能变成 CP 系统——只能大幅缩小概率。
主库:BGSAVE 生成 RDB + 缓冲新写命令 → 回 +FULLRESYNC → 传 RDB → 补发缓冲命令。从库:清空旧数据 → 加载 RDB(短暂阻塞)→ 接收增量。多个从库并发请求只做一次 BGSAVE。
replid 匹配(主/副 ID 都认)且 offset 仍在主库 backlog 环形缓冲范围内 → +CONTINUE 只补增量;否则 +FULLRESYNC。replid 标记数据历史,offset 是逻辑时钟,backlog 是断线容忍的预算。
默认 1MB(redis.conf 8.2),生产必调。估算:平均写速率 × 预期最长断线时间,留 2 倍余量——如 5MB/s × 60s → 至少 300MB。repl-backlog-ttl 3600:无从库 1 小时后释放。
同步期间主库还要服务写,从库输出缓冲涨过 client-output-buffer-limit replica(256mb/64mb 60s)→ 断开 → 重新全量 → 再超限,死循环。破法:无盘复制、低峰同步、调大缓冲限制、控制单库规模。
quorum(每 master 配置)只决定 sdown 是否升级 odown——确认"真挂了";实际执行故障转移需 leader 获得哨兵多数派授权。5 哨兵 quorum=2:2 个就能确认下线,仍需 3 个授权才能切换。
先淘汰:断线超 down-after-ms×10 + sdown 时长的不合格。再三级排序:replica-priority 小者优先(0=永不晋升)→ replication offset 更大者优先 → runid 字典序小者优先(只为确定性)。
旧主被隔离在少数派分区继续接受写,恢复后被降级清空——期间写入全丢。缓解:min-replicas-to-write 1 + min-replicas-max-lag 10,旧主 10 秒内拒写。仍有两个窗口:拒写前延迟 + 异步复制固有间隙——AP 系统丢数据无法根除。
Interview QA · 2/2
HASH_SLOT = CRC16(key) mod 16384(XMODEM CRC16 取 14 位)。antirez 原答:心跳包带完整槽位图,16384 bit=2KB,65536 bit=8KB 属禁止性带宽开销;且集群不建议超 1000 主节点,槽再多无意义。
MOVED:槽已永久易主,客户端更新槽表再去新节点。ASK:槽迁移中,只对下一条命令生效,须先发 ASKING 打一次性标记,不更新槽表——因为下一条的 key 可能仍在源节点,必须先试源。
目标 IMPORTING + 源 MIGRATING → GETKEYSINSLOT 分批取 key → MIGRATE 原子搬运(序列化+两端短暂锁定+删源,key 任一时刻只在一边)→ SETSLOT NODE 收尾。全程不停服;迁移中跨节点多 key 命令返回 TRYAGAIN。
所有 key 必须同槽否则 CROSSSLOT 错。hash tag:key 中 {} 内第一段参与哈希,{user1000}.a 与 {user1000}.b 同槽。代价是该聚合单元放弃槽间均衡,热点 tag 造成热槽——tag 用业务聚合单元粒度。
主库 FAIL 后从库按 offset 排 rank,延迟 = 500ms + rand(0~500) + rank×1000ms 后发起选举,向全部 master 请求投票,多数授权即晋升,生成更大 configEpoch。不同:投票者是 master 节点而非哨兵;选举内建于 gossip,无需独立进程。
数据量与写压力在单机范围内 → 主从+哨兵(客户端简单、事务 Lua 无限制)。超出单机内存或写上限、需要水平扩展 → Cluster(牺牲跨槽多 key/事务,客户端要 cluster 模式)。客户端分片是历史方案,新项目不选。
会:异步复制 + last failover wins。保护:主库联系不上多数 master 超过 cluster-node-timeout(默认 15s)即停写——少数派最多写 15 秒;多数派侧恢复需 FAIL 广播 + 从库选举(通常 1~2 秒)。cluster-require-full-coverage yes 时槽未全覆盖也拒写。
可以:replica-read-only 默认 yes;Cluster 从库需先发 READONLY 才能读可能过期的数据。过期键:从库不主动删,主库过期/淘汰时合成 DEL 传播;从库用逻辑时钟对读"隐藏"已过期键,晋升主库后才开始独立删除。
Related · Cross-links
persistence · RDB/AOF/混合持久化(全量同步的 RDB 就是 BGSAVE 产物;RDB 存 replid 支撑部分重同步)
cache-patterns · 缓存三大问题与双写一致性(主从读扩展与热 key 治理联动)
distributed-lock · Redis 分布式锁(主从异步复制正是锁失效与 Redlock 之争的根源)
thread-model · 单线程模型(复制流发送、gossip 收发的执行者)
MySQL 主从复制:异步复制 + 丢写窗口两平台同源;Redis 用 backlog 补增量 ↔ MySQL 用 binlog position/GTID
通用线索:异步复制的可用性代价、多数派授权防脑裂、分片坐标与数据解耦(slot ↔ 分库分表路由)
分布式系统对照:哨兵/Cluster 的 epoch ↔ Raft term——"本地怀疑 + 多数确认"是通用故障检测范式
References
本 deck 全部结论可溯源至下列一手材料——数字与版本结论以原文为准。
| redis.io/docs/latest/operate/oss_and_stack/management/replication/ | 三大机制、full sync 流程、replid/offset 语义、主副 replid、diskless、min-replicas、过期键传播 |
| redis.io/docs/latest/operate/oss_and_stack/management/sentinel/ | SDOWN/ODOWN、quorum vs majority、leader 选举与 config epoch、replica selection(priority/offset/runid)、TILT、Consistency under partitions(脑裂场景原文) |
| redis.io/docs/latest/operate/oss_and_stack/management/scaling/ · reference/cluster-spec/ | 16384 slot、CRC16 mod 16384、hash tag 规则、MOVED/ASK、MIGRATING/IMPORTING、gossip 四消息、replica election(rank 延迟公式)、node timeout |
| github.com/redis/redis/issues/2576 | antirez 解释为什么 16384 槽:心跳槽位图 2KB vs 65536 的 8KB、~1000 节点上限 |
| github.com/redis/redis · 8.2 redis.conf | 默认值核对:repl-backlog-size 1mb、repl-backlog-ttl 3600、repl-diskless-sync yes/delay 5、repl-timeout 60、client-output-buffer-limit replica 256mb 64mb 60、replica-priority 100、min-replicas-to-write 0 / min-replicas-max-lag 10、cluster-node-timeout 15000、cluster-replica-validity-factor 10、cluster-migration-barrier 1、cluster-require-full-coverage yes |