架构设计

DDIA 第五章:复制|学习笔记|2026-09-11

围绕单主、多主与无主复制,掌握同步/异步复制、复制延迟与三种读取保证、故障转移和脑裂、写冲突、Quorum、读修复与反熵、Sloppy Quorum、Hinted Handoff、并发写入与版本向量,以及部分写入失败后的幂等重试和强一致性取舍。

🔁 本章核心:复制不是简单地“多保存几份数据”,而是在延迟、可用性、持久性和一致性之间做取舍。 学习日期:2026-09-11 学习方式:场景推演 + 主动回忆问答

一、全章主线

同一份数据存在多个副本后,始终要回答三个问题:

  1. 谁可以接受写入?
  2. 一次写入什么时候算成功?
  3. 副本不一致或并发冲突时怎么办?
复制模型 谁能写 主要优点 主要难题
单主复制 一个 Leader 写入顺序清晰、冲突少 Leader 故障、复制延迟
多主复制 多个 Leader 多地就近写入、适合离线场景 并发写入与冲突解决
无主复制 多个副本 没有固定主节点、故障下仍可写 Quorum、版本比较与副本修复

二、单主复制:Leader 与 Followers

客户端将写入发送给 Leader,Leader 把变更传播给 Followers;读取可以由 Leader 或 Followers 提供。

同步与异步复制

  • 同步复制:等待副本确认后才向客户端报告成功。数据更安全,但延迟更高;同步副本不可用时可能阻塞写入。
  • 异步复制:Leader 保存后立即报告成功。延迟较低、可用性较好,但 Leader 在复制完成前永久损坏时,已确认写入仍可能丢失。

实际系统常采用折中方案:至少一个同步副本,其余副本异步复制。

故障转移与脑裂

Leader 失效后的故障转移通常包括:

  1. 判断 Leader 已失效。
  2. 选择一个 Follower。
  3. 将其提升为新 Leader。
  4. 让客户端和其他副本连接新 Leader。

如果旧 Leader 只是网络隔离且仍接受写入,同时系统已经提升了新 Leader,就会出现 split brain(脑裂)。两个 Leader 各自接受写入后可能产生冲突。

三、复制延迟与读取保证

异步副本不会瞬间追上 Leader,因此会产生三类异常:

保证 含义 违反时的现象
读己之写 自己的更新不能立即消失 刚把昵称改成 B,刷新又看到 A
单调读 已经见过的新版本不能倒退 先看到版本 10,随后看到版本 8
一致前缀读 具有因果关系的事件不能颠倒 先看到订单发货,后看到订单创建

常见解决思路:

  • 写入后的一段时间内读取 Leader。
  • 记录写入版本,只读取已经追上该版本的副本。
  • 将同一用户固定路由到同一副本,提供单调读。
  • 让具有因果关系的数据进入同一分区或使用能够保持因果顺序的机制。

四、复制日志的实现方式

方式 复制内容 特点
语句复制 SQL 语句 简单,但 RANDOM、NOW 等非确定性操作可能导致副本不同
WAL 复制 存储引擎底层变更 精确,但与存储格式和数据库版本紧密绑定
逻辑日志复制 行级插入、更新和删除 与存储引擎解耦,更适合滚动升级和外部消费
触发器复制 应用定义的变更记录 灵活,但复杂、开销较大且更容易出错

添加新 Follower

  1. 在 Leader 上创建一致性快照。
  2. 记录快照对应的复制日志位置。
  3. 将快照复制到新 Follower。
  4. 重放快照之后产生的日志。
  5. 追上 Leader 后开始实时复制。

例如快照位于版本 100,而 Leader 已前进到版本 130,新 Follower 应重放 101~130 的日志,而不是要求业务停止写入。

五、多主复制与冲突

多主复制适合多数据中心、离线客户端和协作编辑,但多个 Leader 可能同时修改同一对象。

例如两个客户端都从昵称 A 开始,分别写出 B 和 C。两次写入彼此不知道,因此发生冲突。

常见处理方式:

  • 避免冲突:同一用户或同一记录固定写入同一个 Leader。
  • LWW:按时间戳只保留一个值,实现简单但会静默丢失写入,且依赖机器时钟。
  • 业务合并:购物车可以合并新增商品;收货地址等单值通常不能简单合并。
  • 保留冲突版本:让应用或用户稍后解决。

多主拓扑包括环形、星形和全连接。全连接通常更能容忍单条链路故障,但消息可能通过不同路径乱序到达。

六、无主复制与 Quorum

设:

  • n:副本总数。
  • w:写入成功所需的副本确认数。
  • r:读取时查询的副本数。

经典条件:

w + r > n

其目的是让读取集合与写入集合至少重叠一个副本。例如 n=3、w=2、r=2,理论上读取至少能碰到一个参与最新写入的副本。

性能与可用性取舍:

  • w 高:写入更慢,但读取可以更快。
  • r 高:读取更慢,但写入可以更快。
  • n=3、w=3、r=1 适合读多写少,但任意一个副本不可用都会阻塞写入。

重要限制:w + r > n 不是绝对强一致性保证。Sloppy Quorum、并发写入、版本判定、部分写入失败和故障恢复仍可能产生旧值或冲突。

七、副本恢复机制

  • Read Repair(读修复):读取时发现旧副本,顺便更新它。长期无人读取的数据可能永远得不到修复。
  • Anti-Entropy(反熵):后台持续比较副本并复制缺失数据,不依赖用户读取。
  • Sloppy Quorum(宽松法定人数):正式副本不可用时,使用其他可用节点临时满足副本数量。
  • Hinted Handoff(提示移交):正式副本恢复后,临时节点将数据交还给它。

宽松 Quorum 提高写入可用性,但读写集合可能不在正式副本上重叠,进一步削弱简单 Quorum 公式的保证。

八、并发写入与版本向量

判断两个写入关系的核心不是墙上时钟,而是因果关系:

  • 新写入知道旧写入:旧写入发生在新写入之前,新版本可以替代旧版本。
  • 两个写入互不知道:两者并发,系统需要同时保留或按照业务规则合并。

示例:

    ┌→ B → D
A ──┤
    └→ C

D 由 B 演化而来,因此 D 可以替代 B;D 和 C 仍然并发,应保留 D 与 C。

版本向量用于记录版本的祖先关系,帮助系统判断一个版本是否支配另一个版本,或两者是否并发。

九、Quorum 写入失败为何仍可能看见新值

当 n=3、w=2 时,如果只有副本 A 写入 B 成功,B、C 写入失败,客户端会因为未达到两个确认而收到失败。但 A 中的 B 通常不会自动回滚,稍后的读取仍可能发现它。

因此:

写入失败只能说明没有达到成功条件,不代表所有副本都没有写入。

解决思路:

  1. 为每次操作生成稳定的唯一 request_id。
  2. 超时或结果不确定时,使用相同 request_id 幂等重试。
  3. 通过 Quorum 读取和版本比较确认可观察状态。
  4. 使用读修复和反熵让副本最终收敛。
  5. 对扣款、库存等要求明确提交语义的业务,使用条件写入、事务或基于共识协议的单一写入顺序。

“设置昵称为 B”天然接近幂等;“余额增加 100”必须使用请求 ID 和去重约束,否则重试可能执行两次。直接回滚某个可达副本并不安全,因为失联副本可能已成功,系统也可能存在并发写入。

十、学习问答记录

  • 能判断同一 Leader 上写后读通常不会出现复制延迟导致的旧值。
  • 能说明同步复制降低数据丢失风险,但可能因副本故障阻塞写入。
  • 能识别脑裂会导致两个 Leader 产生不同数据。
  • 能判断 LWW 会丢弃冲突写入,购物车等场景更适合业务合并。
  • 能使用 w + r > n 分析读写集合重叠。
  • 能区分读修复和反熵,并理解提示移交。
  • 能识别并发分支,判断 B→D 后应保留 C 与 D。
  • 能区分读己之写、单调读和一致前缀读。
  • 能解释逻辑日志为何比 WAL 更适合跨版本复制。
  • 能说明 Quorum 写入失败不等于所有副本均未写入。

十一、易错点

  • 异步写入已确认不等于所有副本已保存。
  • 同步复制更安全,但不是免费的;会增加延迟并降低故障下的写入可用性。
  • LWW 解决了“只保留哪个值”,但没有避免数据丢失。
  • 数据库能够发现冲突,不代表它理解正确的业务合并方式。
  • 同一用户固定读取一个副本主要提供单调读,不自动保证读己之写。
  • Quorum 集合发生重叠,还需要正确判断版本新旧。
  • 写入超时或失败意味着结果未知,不意味着操作一定没有发生。
  • 墙上时钟不能可靠表达因果关系。

十二、复习清单

  • 能比较单主、多主和无主复制
  • 能解释同步与异步复制的取舍
  • 能识别复制延迟导致的三种读取异常
  • 能说明故障转移与脑裂
  • 能比较四种复制日志
  • 能解释多主写冲突与 LWW 的数据丢失风险
  • 能使用 n、w、r 分析基本 Quorum
  • 能区分读修复、反熵、Sloppy Quorum 和 Hinted Handoff
  • 能使用因果关系判断并发写入
  • 能解释部分写入失败后的结果不确定性及解决思路

十三、一句话复盘

复制系统的核心不是让副本永远完全相同,而是在故障、延迟和并发不可避免时,明确写入成功的含义、读取能获得的保证,以及冲突如何被发现和解决。

相关学习