设计一个全球电商「限量发售 + 商品详情 + 评论」混合系统:欧美亚三大洲,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% 的纯展示流量。
原理:一致性模型定义了『多个客户端看到的操作顺序,与某个理想顺序的偏差程度』。从强到弱(每弱一档,性能/可用性升一档):
| 模型 | 承诺 | 实现代价 | 典型 |
|---|---|---|---|
| Linearizable | 所有 op 像在单点按 wall-clock 顺序执行;读必看到最新已提交写 | 多数派同步 + global clock / consensus | etcd, 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-watermark | Twitter 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[兜底]
ConsistentRead=true 升强一致(代价:读容量 ×2、延迟翻倍)。原理:CAP 定理(Brewer 2000,Gilbert & Lynch 2002 证明)说:网络分区(P)发生时,必须在一致性(C)和可用性(A)之间选一个。但这只刻画了『分区时』的行为,没说分区不发生时怎么办——而分区是罕见事件,绝大多数时间系统都不在分区中。
PACELC(Abadi 2012)补全了另一半:Partition 时 A 还是 C;Else(正常时)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 的常见误解逐一拆穿
# ❌ "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) 问题, 不是一致性问题.
原理:因果一致性保证『有因果关系的操作』被所有副本以相同顺序看到。形式定义来自 Lamport 1978 的 happens-before 关系(a → b 当且仅当 a 与 b 在同进程且 a 先发生,或 a 是 b 收到的消息)。
工程实现两个主力:
L = max(local, msg)+1。能保 total order 但无法判断两个 op 是因果还是并发。(phys_time, logical)。具备 vector clock 的因果性 + 接近 wall-clock 的可读性 + 常数 size。CockroachDB、YugabyteDB、MongoDB 5+ 都用 HLC。
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 随节点数膨胀
operationTime(即 HLC),读时 afterClusterTime 强制等齐,保证 RYW + monotonic + read-your-causal。原理:真实系统不会全用一种一致性。按数据的『违反代价』分层是核心方法论。
# 同一请求路径上按业务字段切一致性 (伪代码)
# 场景: 用户在限量商品页点击"立即购买"
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: 超卖.
ONE/QUORUM/ALL、DynamoDB 的 ConsistentRead 按请求选;让业务对每个查询独立决策,而非建多套数据库。time.time()。面试可能追问:
这是典型的『把一致性当默认护身符』的反模式。回答需要把代价用数字摊给业务看,不能讲『工程上更难』这种听不进去的话。
第一步:定位订单的一致性诉求到底是什么。
所以 Linearizable 是过度承诺——它要求『全局观察者按 wall-clock 看到所有订单顺序一致』,而业务根本不消费这个保证。
第二步:数字化代价。假设当前 Postgres semi-sync 主从(PA/EC,跨 AZ 5ms commit),写延迟 P50=8ms / P99=25ms。换 Spanner-style 全球 linearizable:
第三步:给替代方案。真正解决 PM 的『安全』焦虑:
劝说金句:『订单系统的安全不是来自更强的一致性模型,而是来自正确的故障模型 + 对账机制。Linearizable 解决不了你真正担心的"丢订单"——那是 durability 问题不是 consistency 问题』。这一句把术语层级捋清,PM 通常就听懂了。
这道题考察对 commit-wait 机制的深入理解。Spanner 保 external consistency 的关键是:每个事务在分配 commit timestamp t 后,等待真实时间过了 t+ε 才让结果可见。这样保证『所有外部观察者看到的事务顺序匹配真实时间序』。
ε 涨到 200ms 时:
为什么是安全降级:ε 是『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 用更慢换正确,这恰恰证明它的设计哲学』。
这是分布式数据库的核心难题。把三个维度叠加:
传统方案:2PC(Two-Phase Commit)
现代改进:
Trend 是 deterministic 路线,因为:
面试金句:『2PC 不是错,是不够好。分布式事务的未来要么走 deterministic(强语义、高吞吐),要么承认 Saga(弱语义、业务补偿),混着用 2PC 是技术债』。
这是『跨设备一致性』经典难题,session sticky 失效是因为 sticky 通常按 IP 或 cookie 路由,跨设备就跨了。
解法 1:账号级别 last-write LSN(推荐)
users.last_write_lsn = max(current, new_lsn) 到 Redis / Postgres。user.last_write_lsn;副本若落后于这个 LSN,等待或转主。解法 2:客户端 token + 服务端等待(适合实时性高的场景)
解法 3:『写延迟可见』容忍化(产品侧解法)
组合方案:账号级 LSN + UX 兜底。账号级 LSN 解决 95% 场景(< 1 秒可读),剩下 5%(Redis 慢、副本严重 lag)UX 提示『同步中』。纯技术 100% 解决不一致是不经济的。
更深的洞察:跨设备『读自己写』本质是『用户身份在多个会话中表达』,不是分布式系统问题,是『用户身份建模』问题。最优解通常是把『用户的当前状态』作为 first-class entity 持久化(user.last_write_lsn),而不是让每个 session 自己管。这思路推广到 multi-tenant、跨设备同步、协作场景都适用。
工程师本能倾向强一致——『数据对就是底线』。但产品视角看:用户体验到的 SLA 是『系统可用 × 数据正确』的乘积。如果一致性带来频繁不可用,乘积反而更小。
真实推演:某社交 app 跨区域强一致评论系统
反直觉的逻辑:
哪些场景仍必须强一致:
金句:『强一致是为正确性的硬约束买单,不是为"心里舒坦"买单。Spanner 和 Cassandra 不是好坏关系,是适用场景关系。一个有经验的架构师在每个数据域上都做单独决策,而非"全系统强一致"或"全系统最终一致"』。
这也是 CLAUDE.md 强调的『追求超级个体』在系统设计层的体现:把『默认安全』替换为『精确选择』——技术决策的成熟标志。