16.2 跨分片事务、时钟与地理分布
单分片写入只需在一个复制组内达成顺序。跨分片转账则同时涉及多个独立日志:每个分片都可以健康复制,但事务仍可能只在一半分片生效。复制共识和原子提交解决的是两个层次的问题。
2PC 决定全做还是全不做
经典两阶段提交(Two-Phase Commit)由协调者和参与者组成:
Prepare:
coordinator -> participants: canCommit?
participants: persist prepared state and reserve resources
Decision:
all yes -> persist COMMIT decision -> notify all
any no -> persist ABORT decision -> notify all参与者投票 yes 后不能擅自提交或回滚,必须得知最终决定。因此传统 2PC 在协调者故障或网络隔离时可能阻塞,并长期持有锁/intent。现代数据库会把协调状态复制、并行化写 intent、支持恢复查询或使用共识协议缩短不确定窗口,但“跨分片事务没有协调成本”仍不成立。
2PC 只保证原子决定,不独自保证事务隔离。系统仍需 MVCC、锁或时间戳协议处理并发读写。
事务记录与 Write Intent
一个常见实现是先在目标 key 上写 provisional value/intent,并让它指向事务记录:
key A -> intent(value, txn-9)
key B -> intent(value, txn-9)
txn-9 -> PENDING / STAGING / COMMITTED / ABORTED其他事务遇到 intent 时,可以等待、推高时间戳、触发优先级仲裁,或在确认原事务失效后恢复其状态。提交后 intent 被解析为普通 MVCC 版本;回滚则清除。
这些元数据让故障恢复成为协议的一部分,但也解释了为什么长事务和高冲突跨分片写会放大尾延迟。
Serializable 为什么仍会要求客户端重试
分布式数据库会并行执行读写以降低延迟,再在冲突或时间戳无法保持时中止某个事务。成功提交的 Serializable 事务具有串行效果,不代表每次尝试都能成功。
应用应处理产品规定的重试错误,并从事务入口重放:
begin
read all decision inputs
write provisional results
commit
on retryable conflict:
rollback
bounded backoff + jitter
retry the whole transaction跨分片事务越大,参与的 range、网络往返和冲突面越多。优先让业务事务与分片键对齐,再把无法避免的跨分片操作保持短小。
物理时钟为什么不能直接排序事务
机器时钟会漂移,NTP 只能把误差控制在范围内,不能让所有节点看到完全相同的瞬间。若直接把本地 now() 当全局提交顺序,两个区域可能给真实先后相反的时间戳。
常见方法包括:
- Hybrid Logical Clock:把物理时间与逻辑计数结合,保留因果单调性并限制与墙钟偏差;
- TrueTime 类区间:返回
[earliest, latest]不确定区间,协议等待不确定性过去再对外确认; - 中心化/分片化时间戳服务:用协调服务分配可比较版本;
- 共识日志位置:在单复制组内提供天然顺序。
时间戳选择必须与事务协议、MVCC 和故障模型共同讨论。
Spanner 的关键不是“用了原子钟”一句话
TrueTime 向 Spanner 提供带误差界的时间区间。读写事务结合并发控制、复制和 commit wait,让提交时间戳尊重外部可观察的真实先后;MVCC 允许在指定时间戳读取一致快照。
外部一致性比普通可串行化多一个实时顺序约束:若 T1 已经提交完成后 T2 才开始提交,所有观察者不能把 T2 排在 T1 前面。
这并不意味着所有全球事务都没有跨区延迟。写延迟仍取决于副本布局、参与分片和同步确认;部署要在写入地区、读延迟和故障域目标之间选择。
CockroachDB 的 Range 视角
CockroachDB 把排序 KV 空间切成 range,每个 range 用 Raft 复制;通常由 leaseholder 负责最新一致读写。SQL 事务跨 range 时,事务层协调 write intent 与事务状态,支持全局 ACID 语义。
陈旧 follower read 可在不超过 closed timestamp 的时间点从本地副本读取;它是明确时间戳的一致历史快照,不等于随意从异步副本读“差不多新的数据”。
产品实现持续演进,因此课程重点应放在层次关系:SQL → 事务 → 分布/路由 → 每 range 复制 → 本地存储,而不是背诵某个版本的内部 RPC 次数。
数据局部性就是延迟设计
地理分布表通常需要选择:
- 单区域写主地:本地写快,远端写跨区;
- 按行/租户归属区域:各自本地访问快,跨归属事务昂贵;
- 全局读取副本:读近,但同步副本越多写入越慢;
- 有界陈旧 follower read:读近且不挡写,但看到历史时间点。
设计评审应拿真实事务图回答:一次请求触及哪些键、这些键的 lease/leader 在哪里、需要几个故障域确认、最坏跨区往返是多少。
什么时候不该用跨分片事务
若业务允许异步完成,可用本地事务 + outbox 发布事实,再由幂等消费者更新派生状态。但 Saga/补偿不是 ACID 事务的同义替代:中间状态会对外可见,补偿可能失败,业务必须定义可观察状态机。
不可逆的余额、配额和唯一性约束不要为了“解耦”随意拆成最终一致消息;先证明业务可以容忍中间状态和补偿语义。
参考
- CockroachDB:Transaction Layer
- Cloud Spanner:TrueTime and External Consistency
- Cloud Spanner:Transactions
下一章沿着事务提交后的边界继续:如何把变化可靠地交给其他系统,并正确处理重复、乱序与积压。