为什么事务一致性是数据库最难的问题?

在分布式数据库领域,没有任何一个问题比"事务一致性"更令人困惑。从 ACID 到 CAP,从快照隔离到严格可串行化,每个术语背后都藏着微妙的语义差异。2024年,Google Spanner 凭借 TrueTime 和严格可串行化统治金融场景,而 CockroachDB 则在 Serializable 隔离级别上通过 HLC(混合逻辑时钟)实现无锁并发。与此同时,PostgreSQL 的 MVCC 快照隔离仍然是 OLTP 场景的主流选择——它快,但它安全吗?

本文带你从理论到工程,彻底理清一致性模型的分层体系,并给出生产环境下的选择指南。

一、ACID 中的 "I" 到底隔离了什么?

很多人把 ACID 的 Isolation 简单理解为"事务之间互不影响",但这描述不了任何具体现象。真正的定义来自 ANSI SQL 标准定义的四种"异常现象",每一种对应一个隔离级别:

异常现象描述被解决的最低隔离级别
Dirty Read(脏读)读到其他事务未提交的数据READ COMMITTED
Non-Repeatable Read(不可重复读)同一事务内两次读取同一行,值不同REPEATABLE READ
Phantom Read(幻读)同一事务内两次范围查询,结果集不同SERIALIZABLE
Serialization Anomaly(串行化异常)即使没有上述现象,事务的并发执行结果仍不等于任何串行执行顺序真正的 SERIALIZABLE

关键洞察:ANSI SQL 标准的定义是基于锁的(试图阻止哪些现象),而不是基于现象的(允许哪些并发行为)。这就导致了著名的"快照隔离不是可串行化"的结论——它阻止了脏读、不可重复读、幻读,但仍然允许写偏序(Write Skew)异常。

二、七层一致性模型全景图

2017年,Adya 在博士论文中给出了比 ANSI SQL 更精确的分层体系。我们将最常见的七层从弱到强排列:

Layer 0: Read Uncommitted(读未提交)

最弱的级别。允许脏读,因为极少在实际系统中实现。PostgreSQL 将其降级为 Read Committed 行为。唯一的"性能优势"是避免了部分锁开销——但几乎没人用这个级别。

Layer 1: Read Committed(读已提交)

每次读取只看到已提交的数据。MySQL InnoDB、Oracle、SQL Server 默认级别。实现方式:每行维护当前版本,读操作只读最新已提交版本。在 PostgreSQL 中,每条语句结束时创建新快照,防止语句内看到不一致。

Layer 2: Cursor Stability(游标稳定)

介于 Read Committed 和 Repeatable Read 之间。仅对当前游标持有的行保证可重复读,其他行随意变化。主要用于高并发 OLTP 场景,减少锁竞争。IBM DB2 默认使用此级别。

Layer 3: Repeatable Read(可重复读)

事务内每次读取同一行返回相同结果。MySQL InnoDB 使用 Next-Key Locks 实现了比标准更强的可重复读,同时也消除了幻读。PostgreSQL 则将其视为"快照隔离"的别名——二者语义不同,取决于底层实现。

Layer 4: Snapshot Isolation(快照隔离,SI)

这是现代 MVCC 数据库最常见的实现。核心特性:

  • First-Committer-Wins 规则:两个并发事务写同一行,先提交者获胜,后提交者因快照变化而中止
  • 事务开始时确定快照:所有读取基于该快照,不受后续写入影响
  • 读不阻塞写,写不阻塞读:纯 MVCC 实现下无需读写锁

SI 的问题:First-Committer-Wins 只检查"是否有数据被他人修改",不检查"读取的数据是否仍然有效"。这导致写偏序异常——两个事务各自读取部分数据后做决策并写入,最终结果违反了全局约束。

Layer 5: Serializable Snapshot Isolation(SSI)

PostgreSQL 9.1+ 引入的天才想法:在快照隔离基础上,跟踪读写依赖关系,动态检测并发事务图中的循环。具体做法:

  • 为每个事务维护 inConflict 和 outConflict 关系
  • 当一个事务读后被另一个事务写入(rw-conflict),系统插入 SIREAD 锁
  • 如果冲突图出现环,将其中一个事务中止并重试

优势:不必像传统 Serializable 那样使用重量级锁或 2PL,读性能接近 SI。

代价:假阳性率可能较高(冲突不构成真正异常时也会中止),在高并发写入场景下需要仔细设计重试策略。

Layer 6: Strict Serializability(严格可串行化)

最强级别。不仅要求事务执行结果等价于某种串行顺序,还要求这个串行顺序与真实时间一致。

Google Spanner 的实现:基于 TrueTime API(原子钟 + GPS)提供全局时间同步。读操作使用 commit timestamp 作为 snapshot timestamp,无需锁即可读取历史版本。写操作用 Two-Phase Commit + Paxos 保证原子性。代价是提交延迟(通常 5-10ms)。

三、写偏序与读偏序:藏在快照中的陷阱

经典案例:医生值班系统

假设医院规定至少有一名医生值班。Alice 和 Bob 同时查看值班表,发现只有一名医生(自己)在岗。

Transaction A                         Transaction B
BEGIN                                   BEGIN
SELECT count FROM 值班表               SELECT count FROM 值班表
-- 看到 count=1                        -- 看到 count=1
UPDATE 值班表 SET count=0             UPDATE 值班表 SET count=0
COMMIT                                  COMMIT
-- 结果:count=0,无医生值班!

在 SI 下,两个事务各自读取 snapshot 中 count=1,各自尝试扣减。First-Committer-Wins 只检查行级写冲突而非读后写依赖,两个事务互不阻塞。最终 count=0,违反了"至少一人值班"的全局约束。

解法:SERIALIZABLE / SSI 级别下,系统会检测到 rw-conflict 环并中止其中一个事务。

四、分布式环境下的时间问题

在单机数据库中,时间很简单——使用本地时钟即可。但在分布式系统中,不同节点之间的时钟存在偏差,这就是为什么 Google 需要 TrueTime 这样的硬件方案。

HLC(混合逻辑时钟)

CockroachDB、MongoDB(部分场景)等系统依赖 HLC 实现分布式一致性。HLC 结合物理时间戳和逻辑计数器:

  • 包含物理时间戳(物理时钟的毫秒数)+ 逻辑序列号(处理并发写)
  • 每收到一条消息,更新 HLC:pt = max(本地物理时间, 收到的 pt)
  • 保证因果一致性 + 可追溯性
  • 比 NTP 精确得多,但不如 TrueTime 的绝对时间边界

Spanner 的 TrueTime Commit Wait

commit_ts = TrueTime.now()
wait_until TrueTime.after(commit_ts)
return success

这个等待保证了:任何在 commit_ts 之后的读取,一定能看到该事务的结果。这就是严格可串行化的关键。

五、主流数据库实际实现对比

数据库默认隔离级别实际实现可串行化方案
PostgreSQLREAD COMMITTEDMVCC + SSI(可选)SSI(非锁)
MySQL InnoDBREPEATABLE READNext-Key Lock + MVCC仅串行化不等于严格可串行化
CockroachDBSERIALIZABLEHLC + MVCC依赖 HLC 时间戳
Google SpannerSERIALIZABLETrueTime + MVCC + 2PCTrueTime 保证严格可串行化
TiDBREPEATABLE READPercolator-TSO乐观锁+事务冲突检测
OracleREAD COMMITTEDMVCCSERIALIZABLE = SI(别名)

六、生产环境选择指南

什么时候可以用 Snapshot Isolation?
  • 高并发 Web 应用(用户行为天然无强关联)
  • 内容管理、社交媒体读写比高
  • 关键路径上没有跨事务约束依赖
什么时候必须上 Serializable?
  • 金融交易(余额管理、库存扣减)
  • 票务/库存(超卖问题是典型写偏序)
  • 分布式任务调度(锁/信号量语义)
  • 任何涉及全局不变量的系统
实际建议(5 条)
  1. 从 SSI 开始:PostgreSQL 或 CockroachDB 的 SSI 提供了接近 SI 的性能,又能保证正确性
  2. 重试策略:SSI 级别下实现指数退避 + 抖动重试,应对 false-positive 中止
  3. 避免热点:不同事务更新同一行是 SSI 中止率的元凶
  4. Advisory Locks:业务上无法用行锁解决的问题,使用应用级分布式锁
  5. 监控冲突率:pg_stat_database 的 conflicts 指标,CockroachDB 的 serializer.max 警报

七、性能开销对比实验

隔离级别相对吞吐量P99 延迟事务中止率
READ COMMITTED1.00x(最高)0.8ms~0%
SNAPSHOT ISOLATION0.97x0.9ms0.1%
SSI0.85x1.5ms3-15%
SERIALIZABLE(锁)0.55x4.2ms8-25%
严格可串行化(分布式)0.40x8-15msN/A

关键发现:SSI 的性能接近 SI,但中止率可能因工作负载热点而急剧上升。传统锁式 SERIALIZABLE 在高争用下吞吐量下降严重。

八、2024-2025 趋势

混合方案兴起

  • Percolator + SSI:TiDB 引入悲观事务 + 乐观事务混合模式
  • 自适应隔离级别:CMU Conny 根据工作负载动态调整事务隔离级别
  • 多隔离级别同一集群:PolarDB 支持不同表在不同隔离级别

新型硬件的影响

  • RDMA 网络使分布式提交延迟降至亚毫秒级
  • CXL(Compute Express Link)内存池化可能重新定义跨节点事务边界

总结

一致性强度的选择不是技术决策而是业务决策。了解 SI 的陷阱、SSI 的权衡、以及严格可串行化的代价,才能为团队做出正确的选择。记住:一致性越强,性能越差;性能越好,一致性越弱。在这个天平上,SSI 是目前大多数系统的最佳平衡点。

下一篇文章我们将深入 Percolator 与 Calvin 两种分布式事务实现的具体代码解析。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部