Skip to content
横幅:事务简史——从 ACID 到分布式事务

事务简史——从 ACID 到分布式事务 ​

事务是数据库最复杂也最核心的话题——从ACID的四个字母到分布式事务的两阶段提交,每一步都是权衡。

前两篇文章我们聊了复制和分区——数据怎么在多台机器上分布。但有一个根本问题还没有回答:当多个客户端同时读写数据时,怎么保证数据不出错?

这个问题,就是事务(Transaction) 要解决的。

事务是数据库最复杂也最核心的话题之一。它把一组读写操作打包成一个逻辑单元——要么全部成功,要么全部失败。有了事务,应用开发者就不用担心“写到一半断电了怎么办”“两个用户同时改同一条数据怎么办”这类问题了。

但事务不是天然存在的——它是为了简化应用编程模型而人为创造出来的。为了理解它为什么这么复杂,我们需要从它的起源讲起。

一、为什么需要事务? ​

在数据系统中,什么事情都可能出错:

  • 系统侧:数据库软件或硬件随时可能失效,包括写操作进行到一半的时候
  • 应用侧:应用程序可能随时崩溃,包括在一系列操作执行到中间某一步的时候
  • 网络侧:应用和数据库之间、数据库多个节点之间的连接随时可能中断
  • 并发侧:多个客户端同时写入时,可能产生竞态条件和相互覆盖

如果没有事务,应用层就需要自己处理以上所有问题——这几乎是不可能的任务。

复杂度不会凭空消失,只会发生转移。事务就是数据库把复杂度从应用层转移到数据库层的一种机制。

有了事务,应用层只需要做一件事:执行事务,如果失败了就重试。

二、ACID:事务的四个承诺 ​

说到事务,大家第一反应就是 ACID。这四个字母由 Theo Härder 和 Andreas Reuter 于 1983 年提出,用来描述数据库事务的容错保证。

但 DDIA 指出一个很扎心的事实:不同数据库对 ACID 的支持并不相同,尤其是 Isolation(隔离性)。如今,ACID 更多时候成了一个 PR 术语。

所以,我们需要逐一搞清楚每个字母真正的含义。下面这张表给出了四个特性的核心含义和负责方:

特性核心含义谁的责任
原子性(A)一组操作要么全部完成,要么全部不完成(侧重故障恢复)数据库
一致性(C)数据始终满足应用定义的不变量(如账户总和为0)应用程序
隔离性(I)并发事务之间相互隔离,不互相干扰数据库
持久性(D)事务提交后,修改永久保存(即使崩溃也不丢失)数据库

A——原子性(Atomicity) ​

“原子”这个词,在计算机科学中有两种完全不同的含义:

  • 并发编程中的原子性:一个线程执行的操作,另一个线程看不到中间状态
  • ACID 中的原子性:一组操作要么全部完成,要么全部不完成

ACID 的原子性不是关于并发的——那是隔离性(I)的事。ACID 的原子性是关于故障恢复的。

如果一个事务在执行过程中发生了故障,原子性保证已经执行的操作可以被全部回滚(Rollback) 。应用层可以确定:这个事务没有改变任何东西。

举个例子:银行转账——从 A 账户扣 100 元,往 B 账户加 100 元。如果第二步失败了,原子性保证第一步也会被撤销。A 的钱不会莫名其妙地消失。

C——一致性(Consistency) ​

一致性是 ACID 里最特殊的一个字母。

原子性、隔离性、持久性都是数据库的属性——数据库自己就能保证。但一致性不是数据库的属性,而是应用程序的属性。

一致性指的是:数据必须始终满足应用程序定义的一组特定约束(不变量) 。比如“所有账户的借贷总和必须为 0”。

数据库无法阻止你写入违反不变量的脏数据。一致性需要应用自己来保证——你负责正确定义事务,并确保它不破坏不变量。

用 DDIA 的话说:字母 C 不属于 ACID。它是一个应用层的属性,而不是数据库层的保证。

I——隔离性(Isolation) ​

隔离性说的是:多个事务并发执行时,它们应该相互隔离。

最理想的隔离是可串行化(Serializable) ——让所有事务看起来就像一个一个地串行执行一样。这样就不会有任何并发问题。

但问题是:完全的隔离性会导致系统并发性能很低。所以实际数据库中,隔离性是一个光谱——从最弱到最强,有多个级别可以选择。

后面我们会详细展开这几种隔离级别。

D——持久性(Durability) ​

持久性说的是:一旦事务提交成功,它所做的修改就会永久保存。

即使数据库随后崩溃、断电、硬盘损坏,数据也不应该丢失。

在实际实现中,持久性通常通过 WAL(预写日志) 来保证——事务提交前先写日志,日志写成功了才算提交成功。即使系统在写入数据页之前崩溃,重启后也可以通过日志恢复。

三、弱隔离级别:性能与正确性的权衡 ​

理论上,最完美的隔离是可串行化——所有事务串行执行,没有任何并发干扰。

但现实是:可串行化的性能太差了。所以数据库提供了几种弱隔离级别(Weak Isolation Levels) ,让用户在性能和正确性之间做权衡。

读已提交(Read Committed) ​

保证:

  1. 只能读取已提交的数据(防止脏读)
  2. 只能写入已提交的数据(防止脏写)

不保证:

  • 同一个事务中两次读取可能得到不同的结果(不可重复读)

实现方式:行级读锁(防止脏读)+ 行级写锁(防止脏写)。

可重复读 / 快照隔离(Snapshot Isolation) ​

保证:

  • 事务中的读操作看到的是事务开始时的快照,即使数据被其他事务修改了也看不到
  • 防止不可重复读

核心机制:MVCC(多版本并发控制)

MVCC 的核心思想是:保存数据的历史版本。每个事务看到的不是数据的最新版本,而是符合其隔离级别的那个版本。

MVCC 的好处是:读不阻塞写,写也不阻塞读。读写操作之间不再相互等待,并发性能大幅提升。

可串行化(Serializable) ​

保证:

  • 最强隔离级别,完全服从 ACID 的隔离性要求
  • 所有事务依次逐个执行,事务之间完全不可能产生干扰
  • 防止脏读、不可重复读和幻读

实现方式:

  1. 两阶段锁(2PL) :事务在读取或写入数据前必须获取对应的锁,直到事务结束才释放。可以防止所有并发异常,但性能开销大。

  2. 可串行化快照隔离(SSI) :在 MVCC 的基础上增加了冲突检测,发现冲突时中止事务。性能比 2PL 好,是 PostgreSQL 等数据库采用的方式。

隔离级别对比 ​

隔离级别脏读不可重复读幻读并发性能
读未提交可能可能可能最高
读已提交✅ 防止可能可能高
可重复读✅ 防止✅ 防止可能中
可串行化✅ 防止✅ 防止✅ 防止最低

四、分布式事务:当单机不够用了 ​

前面讨论的事务,都是单机事务——所有数据在一台机器上,事务由同一个数据库管理。

但当我们把数据分区到多台机器上之后,一个问题就出现了:一个事务可能涉及多个分区。比如在电商系统中,扣库存(分区1)和创建订单(分区2)需要在同一个事务中完成。

这就是分布式事务(Distributed Transaction) 。

分布式事务的难点在于:多个节点之间需要达成一致。要么所有节点都提交,要么所有节点都回滚。这就是原子提交(Atomic Commit) 问题。

两阶段提交(2PC) ​

两阶段提交(Two-Phase Commit,2PC) 是最经典的分布式事务协议。它引入了一个协调者(Coordinator) 来协调所有参与者的行为。

第一阶段(准备阶段) :

  • 协调者向所有参与者发送事务操作请求
  • 每个参与者评估自己是否能完成事务
  • 如果可以,就锁定资源、执行操作(但不提交)、写入 undo/redo 日志,然后返回“就绪”
  • 如果不可以,返回“失败”

第二阶段(提交/回滚阶段) :

  • 如果所有参与者都返回“就绪”,协调者发送“提交”指令,所有参与者正式提交
  • 如果任何一个参与者返回“失败”,协调者发送“回滚”指令,所有参与者回滚

2PC 的问题 ​

2PC 看起来很完美,但它有一个致命的弱点:协调者是单点。

如果协调者在第二阶段发送“提交”指令之前崩溃了,所有参与者都会一直等待,资源被锁住,系统陷入阻塞。

这个问题在 三阶段提交(3PC) 中试图通过引入超时机制来解决,但 3PC 并没有被广泛采用,因为它引入了新的复杂性问题。

分布式事务的现状 ​

分布式事务在实际生产环境中用得不多。原因很简单:

  • 性能差:2PC 需要多次网络往返,延迟高
  • 可用性低:协调者成为单点故障
  • 实现复杂:需要处理各种边缘情况

所以很多大规模系统放弃了分布式事务,转而采用最终一致性的方案——比如 Saga 模式、TCC 模式、基于消息队列的补偿事务等。

分布式事务是强一致性的代价。在很多场景下,最终一致性已经足够好了,而且性能和可用性都好得多。

五、写在最后 ​

事务是数据库最复杂的主题之一,因为它涉及了并发控制、故障恢复、分布式一致性等多个子领域。

回顾一下这条演化路径:

  • 单机事务(ACID)→ 解决了应用层的编程复杂度
  • 弱隔离级别(读已提交、快照隔离)→ 在性能和正确性之间做权衡
  • 分布式事务(2PC)→ 把事务扩展到多台机器,但代价巨大

事务保证和是否分布式在概念上相对正交,但在实现上,分布式系统中事务的实现难度要大得多。

DDIA 并没有给出“分布式事务很好用”的结论——恰恰相反,它提醒我们:事务是有代价的。在决定使用事务之前,需要确切理解事务可以提供的安全保障,以及它们的代价。

没有银弹。理解权衡,做出选择——这才是事务这个话题给我们的最大启示。

下一篇预告:分布式系统的“部分失效”——网络延迟、时钟与故障检测

Released under the MIT License.