Day 6 Hard Distributed Systems Consistency CAP / PACELC Causal

一致性 (Consistency) — 它不是一个开关,是一整条光谱Linearizable / Sequential / Causal / RYW / Monotonic / Eventual · CAP & PACELC · HLC & Version Vectors

问题场景与约束

设计一个全球电商「限量发售 + 商品详情 + 评论」混合系统:欧美亚三大洲,50 万 DAU,发售时段瞬时 20k 下单 QPS / 200k 商品查看 QPS / 50k 评论读写 QPS。同一个系统里有三类截然不同的一致性要求:

这三套需求强迫你 在同一系统内混用多种一致性模型——这才是真实系统的样子。Day 5 已经讲了复制怎么做;今天讲:哪些一致性保证值得为之付出延迟与可用性,哪些不值,工程上怎么落地

高层架构

graph TD
    subgraph Edge["Edge / CDN"]
        CDN["商品详情页
eventual, TTL 60s"] end subgraph Core["Core (us-east, 强一致区)"] INV["库存服务
Spanner / CockroachDB
Linearizable
"] ORD["订单服务
Postgres semi-sync
RYW via session token
"] end subgraph Comments["评论域 (Multi-Region)"] CMT_US["评论 us-east"] CMT_EU["评论 eu-west"] CMT_AP["评论 ap-south"] HLC[("HLC
Hybrid Logical Clock")] end Client["客户端
携带 session LSN / HLC"] Client -- "view product" --> CDN CDN -. miss .-> INV Client -- "order / decrement stock" --> INV Client -- "list my orders" --> ORD Client -- "post / read comments" --> CMT_US Client -- "post / read comments" --> CMT_EU Client -- "post / read comments" --> CMT_AP INV -. CDC .-> CDN ORD -. CDC .-> CDN CMT_US <-. async + HLC .-> CMT_EU CMT_EU <-. async + HLC .-> CMT_AP CMT_AP <-. async + HLC .-> CMT_US classDef strong fill:#2a1530,stroke:#ff7ab6,color:#e8eef5 classDef ryw fill:#1a2530,stroke:#64c8ff,color:#e8eef5 classDef causal fill:#0e2030,stroke:#5eead4,color:#e8eef5 class INV strong class ORD ryw class CMT_US,CMT_EU,CMT_AP,HLC causal

三种一致性域:库存走全球 linearizable(Spanner 类多数派),订单走单 region 主从 + 客户端 LSN,评论走 multi-region async + HLC 保因果序。Edge CDN 兜住 95% 的纯展示流量。

关键技术点

1. 一致性光谱:6 种模型,从『绝对正确』到『最终对齐』

原理:一致性模型定义了『多个客户端看到的操作顺序,与某个理想顺序的偏差程度』。从强到弱(每弱一档,性能/可用性升一档):

模型承诺实现代价典型
Linearizable所有 op 像在单点按 wall-clock 顺序执行;读必看到最新已提交写多数派同步 + global clock / consensusetcd, ZooKeeper, Spanner
Sequential所有副本看到的 op 顺序相同(但不必匹配真实时间)全局序列化(leader / total order broadcast)Kafka 单 partition, Redis
Causal有因果依赖的 op 保序;并发 op 不约束HLC / version vector + 携带依赖MongoDB causal session, COPS
Read-your-writes自己写完立刻读到session sticky 或客户端 LSN token大多数 web app 的最低标
Monotonic Read读不会『回到过去』客户端粘到 follower / 带 high-watermarkTwitter timeline
Eventual停止写后所有副本最终收敛异步复制 + 收敛函数 (LWW / CRDT)DNS, S3 list, Cassandra default
关键认知:『强一致』不是默认应该选的——是要被论证才能用的。
# 用户看不见的差异 vs 看得见的差异
# 看不见 (但有人为之卖命): linearizable vs sequential 在单数据中心通常无感
# 看得见 (用户立刻投诉):
#   - 我刚下单看不到订单     → 违反 RYW
#   - 评论先看到 5 条又只剩 3 条 → 违反 monotonic read
#   - 回复出现在被回复之前    → 违反 causal (consistent prefix)
#   - 限量 100 件卖出 102 件  → 违反 linearizable

# 工程优先级 (从一定要 → 可选):
#   linearizable[关键资源]  ≫  RYW + monotonic + causal[用户体验]  ≫  eventual[兜底]
现实案例:

2. CAP 与 PACELC:『二选一』是误导,『四选二』才接近真相

原理:CAP 定理(Brewer 2000,Gilbert & Lynch 2002 证明)说:网络分区(P)发生时,必须在一致性(C)和可用性(A)之间选一个。但这只刻画了『分区时』的行为,没说分区不发生时怎么办——而分区是罕见事件,绝大多数时间系统都不在分区中。

PACELC(Abadi 2012)补全了另一半:Partition 时 A 还是 CElse(正常时)Latency 还是 Consistency。完整决策有 4 种组合:

类型分区时正常时典型
PA / EL保可用低延迟(弱一致)Cassandra, DynamoDB, Riak
PA / EC保可用强一致MongoDB default (writeConcern majority)
PC / EL保一致(不可用)低延迟较少见 — 想保 C 通常正常时也要 C
PC / EC保一致强一致Spanner, CockroachDB, etcd, HBase
面试金句:『CAP 是分区时的二选一,PACELC 才告诉你 99% 的时间该如何取舍』。

真实生产决策只有两类 反模式:默认就上 PC/EC 给非关键数据。把『社交评论』放 etcd 是工程灾难。
# CAP 的常见误解逐一拆穿
# ❌ "CAP 让我必须放弃一个" — 错. P 不是『可选』, 是物理事实.
#    真实选择是: P 发生时, 你选 A 还是 C.
# ❌ "我们选了 AP" — 太粗. AP 也分:
#    - read AP, write CP (DynamoDB ConsistentWrite + EventualRead)
#    - per-key 可配 (Cassandra LOCAL_QUORUM)
# ❌ "CAP 是 2 选 1" — 错, 是 P 时 2 选 1; 平时还有 E (latency vs consistency).
# ❌ "强一致 = 高延迟" — 跨区域才如此. 单 region Raft 多数派常 < 5ms.
# ❌ "Eventual = 数据会丢" — 错. eventual 只是『暂时不一致』, 最终收敛.
#    数据丢失是『耐久性』(durability) 问题, 不是一致性问题.
现实案例:

3. Causal Consistency:用 HLC 和 Version Vector 实现『因果序』

原理:因果一致性保证『有因果关系的操作』被所有副本以相同顺序看到。形式定义来自 Lamport 1978 的 happens-before 关系(a → b 当且仅当 a 与 b 在同进程且 a 先发生,或 a 是 b 收到的消息)。

工程实现两个主力:

sequenceDiagram
    participant U as Alice (us-east)
    participant US as Comments US
    participant EU as Comments EU
    participant V as Bob (eu-west)

    Note over U,V: 因果场景: Bob 回复 Alice 的评论
    U->>US: post "好看吗?" HLC=(t1, 0)
    US->>EU: replicate (t1, 0)
    EU->>V: read shows "好看吗?"
    V->>EU: reply "好看" with deps=[(t1,0)] HLC=(t2, 0)
    Note over EU: t2 > t1 保证因果
    EU->>US: replicate reply with deps
    Note over US: 收到 reply, 检查 deps
若 (t1,0) 未到 → 暂存 US-->>U: 现在看 — 先看到"好看吗?", 再看到"好看" ✓ Note over U,V: 错误对比 (无 HLC, 异步复制): Note over US: reply 可能先于 question 到达
导致用户先看到回复, 后看到提问 ✗
# Hybrid Logical Clock (HLC) — 简化伪代码
class HLC:
    def __init__(self):
        self.l = 0      # logical (max wall time seen so far)
        self.c = 0      # counter to break ties

    def now(self):     # local event (e.g. write)
        pt = physical_time_ms()
        if pt > self.l:
            self.l, self.c = pt, 0
        else:
            self.c += 1
        return (self.l, self.c)

    def update(self, remote_l, remote_c):  # receiving msg
        pt = physical_time_ms()
        new_l = max(self.l, remote_l, pt)
        if new_l == self.l == remote_l:
            self.c = max(self.c, remote_c) + 1
        elif new_l == self.l:
            self.c += 1
        elif new_l == remote_l:
            self.c = remote_c + 1
        else:
            self.c = 0
        self.l = new_l
        return (self.l, self.c)

# 性质:
#  - 单调递增 (本地或外部触发都不会回退)
#  - happens-before 关系保留: a→b ⇒ HLC(a) < HLC(b)
#  - 接近 wall clock (差异 bounded by max clock skew)
#  - 常数 size (8 bytes), 不像 vector clock 随节点数膨胀
现实案例:

4. 工程落地:在同一系统里『按数据类型』切一致性

原理:真实系统不会全用一种一致性。按数据的『违反代价』分层是核心方法论。

分层决策表(用 SLO/业务损失驱动)
# 同一请求路径上按业务字段切一致性 (伪代码)

# 场景: 用户在限量商品页点击"立即购买"
def place_order(user, sku, qty):
    # Step 1: 强一致 — 扣库存. 必须 linearizable.
    #   走 Spanner / CockroachDB, 全球多数派, ~30ms 延迟可接受.
    reserved = inventory_db.atomic_decrement(sku, qty)   # CP
    if not reserved:
        return "Sold out"

    # Step 2: RYW — 写订单. session token 让用户立刻看到.
    order = orders_db.insert(user, sku, qty)             # primary write
    session.set_lsn(order.lsn)                            # 客户端带回

    # Step 3: Eventual — 推荐 & 热度计数
    fan_out_async("user.purchased", {user, sku})         # Kafka → 推荐/搜索
    increment_counter_eventual(sku + ":popular", 1)      # Redis, 弱一致 OK

    # Step 4: Causal — 通知 (用户的下游动作要保因果)
    notify_with_hlc(user, "order_placed", deps=[order.hlc])

    return order

# 关键: "立即购买" 一个动作四种一致性, 因为业务诉求不同.
# 一刀切走 Spanner: 慢. 一刀切走 Cassandra: 超卖.
现实案例:

扩展与优化

常见陷阱与面试问题

1. 把 strong consistency 当默认:上来就 Spanner / Raft,结果跨区域写 200ms。先问『违反一致性的代价是什么』,再选模型。
2. 误把 ACID 等于强一致:ACID 是单库事务保证;分布式『C』是跨节点保证。Postgres 单机有 ACID,但跨副本可能是 eventual——别混。
3. 用墙钟实现 ordering:分布式系统时钟漂移 1-100ms,NTP 也无法保证单调。用 HLC / Lamport / version vector,不要 time.time()
4. 以为 quorum (W+R>N) = linearizable:Dynamo-style quorum 在 sloppy quorum + hinted handoff 下可破坏 linearizable。真正 linearizable 需要 read-repair + leader 或 consensus(Raft/Paxos)。
5. 客户端缓存破坏 monotonic read:浏览器缓存了较旧的页面,刷新看见 stale 数据看似系统倒退。前端也是一致性域的一部分,记得清缓存或 ETag。

面试可能追问:

  1. 用 30 秒解释 linearizable、sequential、causal 三者的差异,举各自的『违反场景』。
  2. 你的系统要『库存不超卖 + 评论顺序对 + 推荐快』,画出每一部分的一致性选型与理由。
  3. CAP 定理常被批评为『过时』,PACELC 解决了什么?为什么 99% 时间在 E 而不是 P?
  4. 解释 Hybrid Logical Clock 比 Lamport timestamp 和 Vector Clock 各强在哪?size、可读性、因果判断?
  5. Spanner 为什么能号称『全球 linearizable』?TrueTime 的 ε 是怎么影响 commit 等待时间的?没有 GPS 钟怎么做(CockroachDB)?

深入资源

深入思考

1. 同一笔『购物结账』,库存走 Linearizable、订单走 RYW、推荐走 Eventual。如果某天产品经理说『订单也走 Linearizable 吧,安全点』,你怎么劝?给数字。

这是典型的『把一致性当默认护身符』的反模式。回答需要把代价用数字摊给业务看,不能讲『工程上更难』这种听不进去的话。

第一步:定位订单的一致性诉求到底是什么。

  • 用户写了订单要立刻看到 → RYW(session token 即可)
  • 订单状态不能回退 → Monotonic Read(同上)
  • 同一订单的支付、发货、退款保持因果 → Causal(HLC 即可)
  • 『跨用户的订单顺序』是否必须全局一致?答案是:。用户 A 看到自己的订单顺序、用户 B 看自己的,互不影响。

所以 Linearizable 是过度承诺——它要求『全局观察者按 wall-clock 看到所有订单顺序一致』,而业务根本不消费这个保证。

第二步:数字化代价。假设当前 Postgres semi-sync 主从(PA/EC,跨 AZ 5ms commit),写延迟 P50=8ms / P99=25ms。换 Spanner-style 全球 linearizable:

  • 每次写需要跨区域多数派 ACK + TrueTime commit wait(约 7ms)。P50 涨到 ~80ms(跨洋 RTT),P99 ~200ms
  • 20k QPS 下单(按场景)→ 用户感知延迟从『几乎瞬时』变成『明显卡顿』,结账转化率历史经验下降 5-15%(Amazon 100ms = 1% revenue 那条法则)。
  • 基础设施成本:Spanner-class 数据库 + 全球节点是单 region Postgres 的 5-10 倍。
  • 研发:跨区域强一致写 + idempotency + retry 设计远复杂;事故诊断也更难。

第三步:给替代方案。真正解决 PM 的『安全』焦虑:

  1. 订单写主 Postgres,semi-sync 至少 1 副本 ACK → RPO ≈ 0。
  2. 客户端带 LSN token → RYW + monotonic 保证。
  3. 关键状态变更(待支付→已支付)用 idempotency key 防重;事件流走 Outbox 模式 → 0 数据丢失。
  4. 对账定时跑:订单总额 = 库存出货 × 单价 ± 退款。任何差异告警。

劝说金句:『订单系统的安全不是来自更强的一致性模型,而是来自正确的故障模型 + 对账机制。Linearizable 解决不了你真正担心的"丢订单"——那是 durability 问题不是 consistency 问题』。这一句把术语层级捋清,PM 通常就听懂了。

2. Spanner 的 TrueTime ε(不确定性)大约 7ms。如果某天 GPS 信号被干扰,ε 涨到 200ms,会发生什么?为什么这是『安全降级』而不是『错误』?

这道题考察对 commit-wait 机制的深入理解。Spanner 保 external consistency 的关键是:每个事务在分配 commit timestamp t 后,等待真实时间过了 t+ε 才让结果可见。这样保证『所有外部观察者看到的事务顺序匹配真实时间序』。

ε 涨到 200ms 时

  • 每次事务多等 ~200ms。吞吐量直接砍掉一大块,P99 延迟雪崩。
  • 正确性不受影响——这是关键。Spanner 选择『慢但对』(PC/EC),不会因为 ε 大就放弃 linearizable 承诺。
  • 系统会触发监控告警,运维介入;如果 GPS 修复,ε 回到 7ms 性能自然恢复。

为什么是安全降级:ε 是『clock 不确定性的上界』,Spanner 永远假设最坏情况。ε 大只是『我们承认时钟不准』,commit wait 兜底保证语义。这与『时钟错了但系统不知道』是天壤之别——后者会破坏一致性。

对比 CockroachDB(无 TrueTime):用 HLC + max_offset(默认 500ms)兜底,超过 max_offset 的事务会冲突重试。没有 GPS 钟也能保 serializable,但代价是 max_offset 设得保守,跨区域延迟更高。这是『工程妥协 vs 硬件投资』的经典对比。

更深的洞察:Spanner 把『时钟不确定性』变成『等待时间』——把一个看似无解的物理问题(时钟同步)转化为一个工程可量化的资源(等待)。这是分布式系统设计美学的高峰:不和物理对抗,而是把物理量纳入抽象。HLC 是同一思路的更便宜版本(用 logical counter 弥补 clock skew)。

所以这道题的『正确答案』不是『Spanner 完了』,而是『Spanner 用更慢换正确,这恰恰证明它的设计哲学』。

3. 跨章节:Day 4 (Sharding) + Day 5 (Replication) + Day 6 (Consistency) 一起部署。一个『分片 + 多副本』的 OLTP,怎么在 shard 之间做强一致?为什么 2PC 在这里效率很差,而 deterministic transaction 是 trend?

这是分布式数据库的核心难题。把三个维度叠加:

  • 分片内:单 shard 多副本,Raft / Paxos 多数派达成 linearizable(CockroachDB、TiKV、Spanner 都这么干)。
  • 跨分片:一个事务可能涉及多 shard(如『从用户 A 账户转 100 到用户 B』,A、B 在不同 shard)。

传统方案:2PC(Two-Phase Commit)

  • Phase 1: coordinator 让所有 shard prepare(锁记录、写 prepare log)
  • Phase 2: 全 yes → commit,任一 no → abort
  • 问题:(a)coordinator 是单点;(b)prepare 阶段所有 shard 锁记录,期间任何分片不可服务这些行;(c)协调者挂在 phase 1 与 phase 2 之间 → 阻塞所有参与者;(d)2 个 RTT × N 分片 = 高延迟。

现代改进:

  1. Spanner / CockroachDB 的 2PC + Paxos:coordinator 角色由 Paxos group 担任(高可用);每个 shard 自己也是 Paxos group。仍是 2PC 但参与者都是高可用 group——慢但不阻塞。
  2. Calvin / FaunaDB 的 deterministic transactions:先用全局序列器(log)对所有事务定一个总序,所有节点按相同序执行 → 不需要锁、不需要 2PC,只要每个节点跑出来结果一定相同。优势:吞吐量极高;代价:写要先经全局 log(latency 由 log fan-in 决定),读写要 deterministic(不能依赖随机数/外部 IO)。
  3. Saga 模式(微服务常用):把跨 shard 事务拆成多个本地事务 + 补偿。不是 ACID,是『最终一致 + 补偿』。代价:业务必须设计补偿逻辑(退款、回滚库存)。Day 7 详谈。

Trend 是 deterministic 路线,因为:

  • 云时代『全球序列器』变便宜了(Kafka、Raft log),瓶颈不再是 single coordinator;
  • 2PC 的根本问题(锁 + 阻塞)无法靠工程优化掉,只能换范式;
  • determinism 给 replay / 故障恢复 / 跨区域复制带来额外好处(可重放即正确)。

面试金句:『2PC 不是错,是不够好。分布式事务的未来要么走 deterministic(强语义、高吞吐),要么承认 Saga(弱语义、业务补偿),混着用 2PC 是技术债』。

4. 『读自己写』听起来朴素,但在『用户 A 用手机写,立刻用电脑读』场景下,session 粘性失效。给 3 种工程解法 + 各自代价。

这是『跨设备一致性』经典难题,session sticky 失效是因为 sticky 通常按 IP 或 cookie 路由,跨设备就跨了。

解法 1:账号级别 last-write LSN(推荐)

  • 用户每次写,服务端记 users.last_write_lsn = max(current, new_lsn) 到 Redis / Postgres。
  • 用户任何设备读,先查 user.last_write_lsn;副本若落后于这个 LSN,等待或转主。
  • 代价:每次读多一次 Redis lookup(< 1ms)。Redis 故障时降级『直接读主』。
  • 优点:跨设备语义清晰;账号级粒度比 session 粒度更符合用户心智模型。

解法 2:客户端 token + 服务端等待(适合实时性高的场景)

  • 写后服务端返回 LSN/HLC token,客户端存到本地。
  • 读时 SDK 自动带 token;如果跨设备没有 token 怎么办?降级为读主。
  • 代价:跨设备退化为强一致读,主库压力。
  • 适用:移动 app 单设备主要使用,跨设备是次要场景。

解法 3:『写延迟可见』容忍化(产品侧解法)

  • 承认跨设备会有 1-2 秒 lag,用 UI 设计兜底——『正在同步…』、『下拉刷新』、push 通知到另一设备。
  • 把不一致变成 UX 而非 bug。
  • 代价:产品体验;某些场景(金融、医疗)不能接受。
  • 适用:Notion / Apple Notes / Dropbox 都这么做——『最终一致 + 同步提示』。

组合方案:账号级 LSN + UX 兜底。账号级 LSN 解决 95% 场景(< 1 秒可读),剩下 5%(Redis 慢、副本严重 lag)UX 提示『同步中』。纯技术 100% 解决不一致是不经济的

更深的洞察:跨设备『读自己写』本质是『用户身份在多个会话中表达』,不是分布式系统问题,是『用户身份建模』问题。最优解通常是把『用户的当前状态』作为 first-class entity 持久化(user.last_write_lsn),而不是让每个 session 自己管。这思路推广到 multi-tenant、跨设备同步、协作场景都适用。

5. 反直觉:在『可用性 5 个 9』和『强一致』之间,业务为什么常常应该选可用性?给一个『选强一致结果灾难』的真实推演。

工程师本能倾向强一致——『数据对就是底线』。但产品视角看:用户体验到的 SLA 是『系统可用 × 数据正确』的乘积。如果一致性带来频繁不可用,乘积反而更小。

真实推演:某社交 app 跨区域强一致评论系统

  • 选型:评论走 Spanner-style 全球 linearizable。理由:『评论顺序不能乱』。
  • 正常运行:欧亚用户评论 P99 延迟 300ms(跨洋多数派)。用户感觉『发评论好慢』,参与度 -8%。
  • 某次 us-east ↔ eu-west 网络分区(一年 ~2 次,每次 5-30 分钟):
    • 欧洲多数派不可达 → 欧洲用户无法发评论。
    • 用户看到错误页 / 旋转加载。投诉飙升。
    • 客服成本飙升、社交媒体差评涌现、当日 DAU 下降。
  • 事故后复盘:『评论顺序乱』这个所谓的核心需求,用户实际并不感知——本来 timeline 已经按时间近似排序,因果序错乱 1-2 条的概率极低且不致命。
  • 重选:改 causal+eventual。分区时双边都可写、合并时按 HLC 排序。年均不可用从 60 分钟降到 0;体验流畅;数据『偶尔乱序』完全在用户容忍范围内。

反直觉的逻辑

  1. 『一致性违反』通常是软伤害(用户看到稍微乱序、稍微旧的数据),而不可用是硬伤害(用户什么都看不到、什么都做不了)。
  2. 用户对『一致性 bug』的忍耐度极高(『刷一下就好了』);对『不可用』的忍耐度极低(『这个 app 烂』)。
  3. 强一致代价是分布式系统中最贵的——它要求多数派同步、跨区域 RTT、协调开销,常常吃掉一个数量级的吞吐和延迟预算。

哪些场景仍必须强一致

  • 钱(账户、计费、定价);
  • 唯一性(用户名、订单号、库存);
  • 状态机的关键转换(已支付 vs 未支付);
  • 配置 / 权限(错了影响全局行为)。

金句:『强一致是为正确性的硬约束买单,不是为"心里舒坦"买单。Spanner 和 Cassandra 不是好坏关系,是适用场景关系。一个有经验的架构师在每个数据域上都做单独决策,而非"全系统强一致"或"全系统最终一致"』。

这也是 CLAUDE.md 强调的『追求超级个体』在系统设计层的体现:把『默认安全』替换为『精确选择』——技术决策的成熟标志。