Chapter 09
自测与场景判别
前八章拆开讲了约束、时序、一致性、复制、分区、共识、事务、权衡。这一章不教新东西,只逼你调用它们:三层题库练回忆,五个跨章节场景练判别——后者才是真正检验你是否把整张地图连起来了。
用法
- 先合上前面所有章节,把答案写在纸上或编辑器里,写完再展开文末答案对照。
- 直接展开答案,等于把这一章当成又读了一遍——回忆练习的价值就没了。
- 卡住的题,记下它指向哪一章,回去重读那一节,而不是看答案。
9.1概念层(对应 01–03)
- partial failure 和单机的「进程崩溃」,本质区别在哪?为什么前者更难处理?
- timeout 触发的那一刻,系统能确定对方崩溃了吗?为什么?
- Lamport 时钟下,
C(a) < C(b)能推出 a happens-before b 吗?向量时钟相比多告诉你什么? - 线性一致性比顺序一致性,多要求了什么?
- 为什么因果一致性被称为「分区下可用系统能达到的最强一致性」?
- 「C in ACID」和「C in CAP」是同一个东西吗?各指什么?
9.2原理层(对应 04–07)
R + W > N能保证读到最新写吗?能保证线性一致吗?分别回答。- 为什么说 sloppy quorum 是「持久性手段」而不是「一致性手段」?
- 用
hash mod N分区,为什么加一个节点会引发几乎全量搬迁?一致性哈希怎么把它降到约 K/N? - Raft 的选举限制(只投给日志不旧于自己的 candidate),为什么能保证已提交的 entry 在换届后不丢?
- 同一个 term 里,为什么不可能出现两个都拿到多数派的 leader?
- 2PC 的「阻塞问题」具体指什么?协调者在哪个时刻崩溃会让参与者卡死、且参与者为什么不能自己决定?
- Saga 牺牲了 ACID 里的哪个性质?「补偿」和数据库「回滚」的本质区别是什么?
- 为什么 exactly-once 投递不可能、但 exactly-once 处理可达?后者靠什么实现?
9.3应用判别层(跨 ≥2 章,选方案)
每个场景先判断「该用哪一章的哪个方案、不该用哪个」,再写出理由。这一层是这套教程真正的检验。
- 跨区域库存服务:要求绝不超卖(强一致),可接受偶尔短暂不可用。它的 CAP 取向是什么?该用 04 章哪种复制、配 06 章的什么?为什么不能用无主 + LWW?
- 多人协同文档:用户离线编辑,恢复后合并;要求高可用、低延迟,能容忍短暂不一致。该选 03 章哪种一致性、哪种数据结构?为什么这里不用 06 章的共识?
- 保护「扣款」的分布式锁:要求绝对互斥。该用 06 章的什么锁?朴素 Redis
SETNX行不行?还要在资源侧配合什么? - 多主复制下的并发写:两个数据中心并发改了同一条记录。用 02 章的物理时钟戳 LWW,还是版本向量?各自的后果是什么?这把你推向 03 章的哪种一致性,而不是线性一致?
- 给一批分片选路由:数据量增长快、要频繁加节点。05 章的范围分区 / 一致性哈希 /
hash mod N,选哪个?为什么另外两个在这里是陷阱或不合适?
亲手画一张图
合上教程,在纸上画出整套教程的概念地图——只画 5 层(约束 → 工具 → 一致性目标 → 机制 → 权衡),每层填 1–2 个关键词。画完回到 index 的图 0.1 对照:你画的图里,最顶端是不是只有一个根本约束?「复制 / 分区 / 共识 / 事务」有没有都挂在「一致性谱系」下面、最后汇到 CAP?缺了哪条连线,就是哪一章还没真正连进你的地图。
进阶挑战 · 刚好够不着
给一个「全都要」的需求挑刺
产品经理要一个系统:跨区域部署、强一致事务、永远可用、低延迟、还要便宜。用 CAP / PACELC(08 章)逐条指出哪些要求互相冲突、必须砍掉哪一个。再说说 Spanner 和 Aurora DSQL 是在这组矛盾里做了什么取舍才能落地的。
提示(卡住再展开)
先用 CAP 砍「分区时既强一致又永远可用」;再用 PACELC 的 else 分支指出「强一致」和「低延迟」在平时也冲突。Spanner / Aurora DSQL 都选 PC/EC:分区时牺牲可用,平时付 commit-wait 延迟换全局一致——它们优化的是把这个延迟压小(有界时钟),不是消除它。
答案(三层 + 场景都做完再展开)
概念层
- 单机崩溃是全局可检测的:所有进程共享同一份 RAM 和时钟,要么都活要么都死,CPU 发现硬件错误就 halt。partial failure 下节点只靠消息互相了解,而异步网络对延迟无上界,所以「丢包 / 慢 / 崩 / 在排队」从外部完全一样——失败不可即时判定,这才是难点。
- 不能。timeout 只说明「窗口内没收到回复」,无法区分请求丢、响应丢、节点崩、还是节点活着但慢。这正是 partial failure 的核心。
- 不能。Lamport 只保证
a→b ⟹ C(a)<C(b),反过来不成立——并发事件也会拿到不同的计数值。向量时钟能区分「因果在后」(向量分量全 ≥)和「并发」(互不支配),代价是每条消息带 O(N) 的数组。 - 多要求了实时序:线性一致要求那个唯一全序符合操作的真实发生时间(wall-clock),所以一个写一旦返回,之后任何节点的读都必须看到它;顺序一致只要求存在某个保持各进程程序序的全序,允许读到旧值。
- 因为再强(线性 / 顺序)就要求跨节点协调,分区时无法同时保持可用;因果一致只需追踪 happens-before、不需全局协调,能提供 sticky availability,是分区下可用性的上界。
- 不是同一个东西,只是共用字母。ACID 的 C = 应用级不变量(外键、业务规则),是应用的责任;CAP 的 C = 线性一致(复制一致性)。serializability(隔离最强)与 linearizability(一致性最强)也是正交的两件事。
原理层
- 能(大概率)读到最新写,因为读集与写集至少交叠一个节点带最新值;但不线性一致——它只是集合交集论证,并发操作 + 网络延迟下陈旧副本可能先返回,客户端先读到旧值。
- 因为 sloppy quorum 在凑不齐 home 节点时把写落到任意可达节点,读 home 节点的读集可能根本不含那个替补节点,
R+W>N的交集保证失效;它保证「写总能落到某处」(持久性),不保证「读到最新」(一致性)。 hash mod N在 N→N+1 后,绝大多数 key 的hash%N都变,几乎全部 key 要搬。一致性哈希把节点和 key 放到环上,新节点只接管它与前驱之间那一段,平均只移动 K/N 个 key。- 已提交 = 已复制到多数派。任何新 leader 必须赢得多数派,而任意两个多数派必交叠至少一个节点,那个节点持有该已提交 entry;选举限制让它只投给日志不旧于自己的 candidate,于是当选者必然也含这条 entry。
- 因为每个节点每个 term 至多投一票,而当选需要多数派;两个 candidate 不可能在同一 term 各自拿到多数派(多数派必交叠,交叠节点不能投两次)。
- 协调者在收齐所有 YES 后、广播 commit/abort 决定之前崩溃,参与者卡死:它已投 YES 并持锁,不能单方提交(别人可能要 abort),也不能单方 abort(别人可能已 commit),只能等协调者恢复。
- 牺牲了隔离性(I)。每个本地事务先提交,中间状态对外可见。回滚是数据库把未提交改动原子撤销;补偿是一个新的前向事务做语义抵消——若别人已观察到中间态,补偿无法让它「没发生过」。
- 投递不可能:网络会丢、会重,且无法区分「还没送到」和「已丢失」。处理可达:发送方给每个请求一个唯一幂等键,接收方持久化已处理的键、重复就跳过,把 at-least-once 投递变成 exactly-once 效果。
应用判别层
- CP。用 06 章的共识复制(或单主 + quorum),写要过多数派才确认,分区时少数派侧拒写、宁可短暂不可用也不超卖。不能用无主 + LWW——它不线性一致且会静默丢写,正是超卖根源。
- 选 03 章的因果 / 最终一致 + CRDT。离线编辑要求各端本地先写、恢复后合并,CRDT 靠交换 / 结合 / 幂等的合并无需协调就收敛。不用共识,因为共识要多数派在线且每次写至少一个 RTT,离线 + 低延迟场景下不可行。
- 用 06 章共识支持的锁(etcd / ZooKeeper)。朴素
SETNX不安全:持锁者 GC 暂停超 TTL、或 Redis 主从 failover 在复制传播前发生,都会让两个客户端同时自认持锁。还要在资源侧配 fencing token,存储拒绝带过期 token 的迟到写。 - 选版本向量。LWW 会按物理时间戳取大者,时钟偏几十毫秒就静默丢掉因果更晚的已确认写;版本向量保留并发版本(siblings)交应用合并,保住所有写。这把你推向 03 章的因果一致 + 应用合并,而不是线性一致。
- 选一致性哈希 + 虚拟节点:频繁加节点时只搬约 K/N 的数据、负载还均衡。
hash mod N是陷阱——每次加节点几乎全量搬迁。范围分区适合范围扫描,但时间 / 顺序前缀的 key 会把写压成热点,不适合这个高写入扩容场景。