2. ACID 事务基础:原子性、一致性、隔离性、持久性的定义与在分布式系统中的挑战

好,咱们正式开始聊 ACID。这四个字母,做数据库的没人不知道。但说实话,很多人对它的理解停留在教科书层面——背得出定义,真遇到问题就抓瞎了。

我个人习惯把 ACID 拆成两半来看:A + D 是一对,保证“要么做完,要么别做,做完就不丢”;C + I 是另一对,保证“数据看起来合理,并发不出乱子”。在单机数据库里,这四件事已经够折腾了。到了分布式系统里……嗯,每个字母都变成了一个深坑。

2.1 原子性(Atomicity)——全做或全不做

原子性的定义很简单:一个事务里的操作,要么全部成功,要么全部回滚。不存在“做了一半”的状态。

我在项目中遇到过最典型的场景:转账。A 扣 100,B 加 100。如果扣完 A 的钱之后系统崩了,B 没加上,这 100 块就凭空消失了。原子性就是要杜绝这种事。

核心要点:原子性不是“同时完成”,而是“看起来同时完成”。中间状态对外不可见。

在单机数据库里,原子性靠 undo log 实现。事务执行时先写 undo,记录“如果回滚该怎么恢复”。一旦崩溃,重启后根据 undo 把未完成的事务抹掉。

但在分布式系统里,事情就麻烦了。一个事务可能跨多个节点,每个节点有自己的 undo log。A 节点提交了,B 节点崩了,谁来协调回滚?

这就引出了分布式原子性的核心协议——两阶段提交(2PC)。说白了就是加一个协调者,先问所有人“能不能提交”,都点头了再发正式提交指令。

我曾经踩过的坑:2PC 的协调者是单点。协调者挂了,所有参与者都卡在“预提交”状态,锁不释放,业务直接停摆。后来我们加了协调者高可用,但脑裂问题又冒出来了……

2.2 一致性(Consistency)——数据要讲逻辑

一致性这个词其实被用滥了。ACID 里的 C,指的是事务不破坏数据库的完整性约束。比如账户余额不能为负,A 转账给 B 后总金额不变——这些规则由应用层定义,数据库只负责保证:事务执行前后,约束都成立。

你想想看,这和分布式系统里常说的“最终一致性”根本不是一回事。ACID 的一致性更接近“合法性校验”,而不是“副本同步”。

概念 ACID 一致性 分布式一致性(如 CAP)
关注点 数据完整性约束 副本之间的数据同步
谁负责 应用 + 数据库 分布式协议
典型手段 触发器、外键、CHECK Paxos、Raft、Quorum
失败后果 事务回滚 数据不一致,需修复

在分布式系统里,一致性最大的挑战是:约束本身可能跨节点。比如“用户总资产不能为负”,但用户的资产分布在 5 个分片上。要检查这个约束,就得读 5 个分片的数据,这本身就是一个分布式事务。

我的建议:别在分布式系统里依赖数据库层的一致性约束。把校验逻辑上提到应用层,用补偿事务来处理违规情况。否则你会被跨节点 CHECK 搞疯。

2.3 隔离性(Isolation)——并发不干扰

隔离性说的是:并发执行的事务,彼此感觉不到对方的存在。每个事务都像独占数据库一样。

但完全隔离性能太差。所以 SQL 标准定义了四个隔离级别,允许你牺牲一部分隔离性来换性能:

  • 读未提交(Read Uncommitted)——能读到别人没提交的数据。脏读风险极高,我几乎没见过生产环境用这个。
  • 读已提交(Read Committed)——只能读到已提交的数据。解决了脏读,但不可重复读还在。
  • 可重复读(Repeatable Read)——同一个事务里多次读同一行,结果一致。但幻读可能发生。
  • 可串行化(Serializable)——最高级别,事务完全串行执行。性能最差,但最安全。

在分布式系统里,隔离性遇到了新问题:全局快照。单机数据库做 MVCC(多版本并发控制)很简单,每个事务拿一个单调递增的事务 ID 就行。但分布式环境下,多个节点的时间戳没法对齐。

我记得有一次排查线上问题,发现两个节点上的事务 ID 竟然出现了交叉——A 节点的事务 100 在 B 节点看来比事务 99 还早。结果就是,本该隔离的事务读到了不该读的数据。

解决方案:Google Spanner 用 TrueTime API 给所有事务打上物理时间戳,误差控制在几毫秒内。但大多数系统没这条件,只能用逻辑时钟(如 HLC)或集中式时间戳分配器。

2.4 持久性(Durability)——写进去就别丢

持久性承诺:一旦事务提交成功,数据就不会丢失——哪怕系统立刻崩溃、断电、重启。

单机数据库靠 WAL(Write-Ahead Logging)实现。数据先写日志,日志落盘了才算提交成功。真正数据页的写入可以慢慢来,崩溃后靠 WAL 重放恢复。

分布式系统里,持久性变成了多副本持久。一个事务提交,意味着数据在 N 个节点上都写入了 WAL。但这里有个微妙的问题:

  • 如果要求所有副本都确认才返回成功,延迟高,且一个节点挂了整个系统不可用。
  • 如果只等多数副本确认就返回,那少数派节点上的数据在故障时可能丢失。

这就是 Quorum 机制 的权衡。我一般建议:对于核心金融数据,用 3 副本 + 全部确认;对于日志类数据,2 副本 + 多数确认就够了。

我曾经犯过的错:为了追求低延迟,把持久化级别从 fsync 降到了 write back。结果一次机房断电,丢了最近 3 秒的所有提交数据。从那以后,我再也不敢在持久性上偷懒。

2.5 分布式系统对 ACID 的冲击——一张图看懂

下面这张图总结了 ACID 在分布式环境下面临的核心挑战和应对思路:

分布式系统对 ACID 的冲击 原子性 (A) 一致性 (C) 隔离性 (I) 持久性 (D) 协调者单点 参与者阻塞 脑裂风险 约束跨节点 校验成本高 状态难对齐 全局快照难 时间戳不一致 网络延迟放大 多副本同步 fsync 性能 故障恢复 2PC / 3PC Saga 补偿 TCC 模式 应用层校验 事件溯源 最终一致性 MVCC + HLC TrueTime 乐观并发控制 Quorum 机制 Raft 日志复制 WAL + fsync 没有银弹,只有权衡

2.6 小结:ACID 在分布式世界里还能用吗?

能,但别硬用。传统数据库的 ACID 是为单机设计的,拿到分布式环境里,每个字母都要重新理解:

  • 原子性——从 undo log 变成了 2PC/Saga,代价是性能和可用性。
  • 一致性——从 CHECK 约束变成了应用层校验,别指望数据库帮你兜底。
  • 隔离性——从本地 MVCC 变成了全局快照,时间戳对齐是最大难题。
  • 持久性——从单机 WAL 变成了多副本 Quorum,fsync 和网络延迟你总得选一个。

数据湖仓的场景更特殊——它要同时处理 OLTP 的小事务和 OLAP 的大批量写入。传统的 ACID 实现根本扛不住。所以像 Apache Iceberg、Delta Lake、Hudi 这些方案,都做了不同程度的妥协和改造。