跳到内容

16.2 跨分片事务、时钟与地理分布

单分片写入只需在一个复制组内达成顺序。跨分片转账则同时涉及多个独立日志:每个分片都可以健康复制,但事务仍可能只在一半分片生效。复制共识和原子提交解决的是两个层次的问题。

2PC 决定全做还是全不做

经典两阶段提交(Two-Phase Commit)由协调者和参与者组成:

text
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,并让它指向事务记录:

text
key A -> intent(value, txn-9)
key B -> intent(value, txn-9)
txn-9 -> PENDING / STAGING / COMMITTED / ABORTED

其他事务遇到 intent 时,可以等待、推高时间戳、触发优先级仲裁,或在确认原事务失效后恢复其状态。提交后 intent 被解析为普通 MVCC 版本;回滚则清除。

这些元数据让故障恢复成为协议的一部分,但也解释了为什么长事务和高冲突跨分片写会放大尾延迟。

Serializable 为什么仍会要求客户端重试

分布式数据库会并行执行读写以降低延迟,再在冲突或时间戳无法保持时中止某个事务。成功提交的 Serializable 事务具有串行效果,不代表每次尝试都能成功。

应用应处理产品规定的重试错误,并从事务入口重放:

text
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 事务的同义替代:中间状态会对外可见,补偿可能失败,业务必须定义可观察状态机。

不可逆的余额、配额和唯一性约束不要为了“解耦”随意拆成最终一致消息;先证明业务可以容忍中间状态和补偿语义。

参考

下一章沿着事务提交后的边界继续:如何把变化可靠地交给其他系统,并正确处理重复、乱序与积压。

Built with VitePress | Software Systems Atlas