为什么事务一致性是数据库最难的问题?
在分布式数据库领域,没有任何一个问题比"事务一致性"更令人困惑。从 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 之后的读取,一定能看到该事务的结果。这就是严格可串行化的关键。
五、主流数据库实际实现对比
| 数据库 | 默认隔离级别 | 实际实现 | 可串行化方案 |
|---|---|---|---|
| PostgreSQL | READ COMMITTED | MVCC + SSI(可选) | SSI(非锁) |
| MySQL InnoDB | REPEATABLE READ | Next-Key Lock + MVCC | 仅串行化不等于严格可串行化 |
| CockroachDB | SERIALIZABLE | HLC + MVCC | 依赖 HLC 时间戳 |
| Google Spanner | SERIALIZABLE | TrueTime + MVCC + 2PC | TrueTime 保证严格可串行化 |
| TiDB | REPEATABLE READ | Percolator-TSO | 乐观锁+事务冲突检测 |
| Oracle | READ COMMITTED | MVCC | SERIALIZABLE = SI(别名) |
六、生产环境选择指南
什么时候可以用 Snapshot Isolation?- 高并发 Web 应用(用户行为天然无强关联)
- 内容管理、社交媒体读写比高
- 关键路径上没有跨事务约束依赖
- 金融交易(余额管理、库存扣减)
- 票务/库存(超卖问题是典型写偏序)
- 分布式任务调度(锁/信号量语义)
- 任何涉及全局不变量的系统
- 从 SSI 开始:PostgreSQL 或 CockroachDB 的 SSI 提供了接近 SI 的性能,又能保证正确性
- 重试策略:SSI 级别下实现指数退避 + 抖动重试,应对 false-positive 中止
- 避免热点:不同事务更新同一行是 SSI 中止率的元凶
- Advisory Locks:业务上无法用行锁解决的问题,使用应用级分布式锁
- 监控冲突率:pg_stat_database 的 conflicts 指标,CockroachDB 的 serializer.max 警报
七、性能开销对比实验
| 隔离级别 | 相对吞吐量 | P99 延迟 | 事务中止率 |
|---|---|---|---|
| READ COMMITTED | 1.00x(最高) | 0.8ms | ~0% |
| SNAPSHOT ISOLATION | 0.97x | 0.9ms | 0.1% |
| SSI | 0.85x | 1.5ms | 3-15% |
| SERIALIZABLE(锁) | 0.55x | 4.2ms | 8-25% |
| 严格可串行化(分布式) | 0.40x | 8-15ms | N/A |
关键发现:SSI 的性能接近 SI,但中止率可能因工作负载热点而急剧上升。传统锁式 SERIALIZABLE 在高争用下吞吐量下降严重。
八、2024-2025 趋势
混合方案兴起
- Percolator + SSI:TiDB 引入悲观事务 + 乐观事务混合模式
- 自适应隔离级别:CMU Conny 根据工作负载动态调整事务隔离级别
- 多隔离级别同一集群:PolarDB 支持不同表在不同隔离级别
新型硬件的影响
- RDMA 网络使分布式提交延迟降至亚毫秒级
- CXL(Compute Express Link)内存池化可能重新定义跨节点事务边界
总结
一致性强度的选择不是技术决策而是业务决策。了解 SI 的陷阱、SSI 的权衡、以及严格可串行化的代价,才能为团队做出正确的选择。记住:一致性越强,性能越差;性能越好,一致性越弱。在这个天平上,SSI 是目前大多数系统的最佳平衡点。
下一篇文章我们将深入 Percolator 与 Calvin 两种分布式事务实现的具体代码解析。

发表评论 取消回复