---
title: "8. 事务"
weight: 208
math: true
breadcrumbs: false
---

<a id="ch_transactions"></a>

![](/map/ch07.png)

> *有些作者声称，支持通用的两阶段提交代价太大，会带来性能与可用性的问题。我们认为，让程序员来处理过度使用事务导致的性能问题，总比缺少事务编程好得多。*
>
> James Corbett 等人，*Spanner：Google 的全球分布式数据库*（2012）

在数据系统的残酷现实中，很多事情都可能出错：

* 数据库软件或硬件可能在任意时刻发生故障（包括写操作进行到一半时）。
* 应用程序可能在任意时刻崩溃（包括一系列操作的中间）。
* 网络中断可能会意外切断应用程序与数据库的连接，或数据库节点之间的连接。
* 多个客户端可能会同时写入数据库，覆盖彼此的更改。
* 客户端可能读取到无意义的数据，因为数据只更新了一部分。
* 客户端之间的竞态条件可能导致令人惊讶的错误。

为了实现可靠性，系统必须处理这些故障，确保它们不会导致整个系统的灾难性故障。然而，实现容错机制需要大量工作。它需要仔细考虑所有可能出错的事情，并进行大量测试，以确保解决方案真正有效。

数十年来，*事务*一直是简化这些问题的首选机制。事务是应用程序将多个读写操作组合成一个逻辑单元的一种方式。从概念上讲，事务中的所有读写操作被视作单个操作来执行：整个事务要么成功（*提交*），要么失败（*中止*、*回滚*）。如果失败，应用程序可以安全地重试。对于事务来说，应用程序的错误处理变得简单多了，因为它不用再担心部分失败——即某些操作成功，某些失败（无论出于何种原因）。

如果你与事务打交道多年，它们可能看起来显而易见，但我们不应该将其视为理所当然。事务不是自然法则；它们是有目的地创建的，即为了*简化应用程序的编程模型*。通过使用事务，应用程序可以自由地忽略某些潜在的错误场景和并发问题，因为数据库会替应用处理好这些（我们称之为*安全保证*）。

并非所有应用程序都需要事务，有时弱化事务保证或完全放弃事务也有好处（例如，为了获得更高的性能或更高的可用性）。某些安全属性可以在没有事务的情况下实现。另一方面，事务可以防止很多麻烦：例如，邮局 Horizon 丑闻（参见["可靠性有多重要？"](/ch2#sidebar_reliability_importance)）背后的技术原因可能是底层会计系统缺乏 ACID 事务[^1]。

你如何确定是否需要事务？为了回答这个问题，我们首先需要准确理解事务可以提供哪些安全保证，以及相关的成本。尽管事务乍看起来很简单，但实际上有许多细微但重要的细节在起作用。

在本章中，我们将研究许多可能出错的案例，并探索数据库用于防范这些问题的算法。我们将特别深入并发控制领域，讨论可能发生的各种竞态条件，以及数据库如何实现*读已提交*、*快照隔离*和*可串行化*等隔离级别。

并发控制对单节点和分布式数据库都很重要。在本章后面的["分布式事务"](#sec_transactions_distributed)部分，我们将研究*两阶段提交*协议和在分布式事务中实现原子性的挑战。

## 事务到底是什么？ {#sec_transactions_overview}

今天，几乎所有的关系型数据库和一些非关系数据库都支持事务。它们大多遵循 1975 年由 IBM System R（第一个 SQL 数据库）引入的风格[^2] [^3] [^4]。尽管一些实现细节发生了变化，但总体思路在 50 年里几乎保持不变：MySQL、PostgreSQL、Oracle、SQL Server 等的事务支持与 System R 惊人地相似。

在 2000 年代后期，非关系（NoSQL）数据库开始流行起来。它们旨在通过提供新的数据模型选择（参见[第 3 章](/ch3#ch_datamodels)），以及默认包含复制（[第 6 章](/ch6#ch_replication)）和分片（[第 7 章](/ch7#ch_sharding)）来改进关系型数据库的现状。事务是这一运动的主要牺牲品：许多这一代数据库完全放弃了事务，或者重新定义了这个词，用来描述比以前理解的更弱的保证集。

围绕 NoSQL 分布式数据库的炒作导致了一种流行的信念，即事务从根本上不可伸缩，任何大规模系统都必须放弃事务以保持良好的性能和高可用性。最近，这种信念被证明是错误的。所谓 "NewSQL" 数据库，如 CockroachDB[^5]、TiDB[^6]、Spanner[^7]、FoundationDB[^8] 和 YugabyteDB 已经证明，事务系统同样可以具备很强的可伸缩性，并支持大数据量与高吞吐量。这些系统将分片与共识协议（[第 10 章](/ch10#ch_consistency)）结合，在大规模下提供强 ACID 保证。

然而，这并不意味着每个系统都必须是事务型的：与任何其他技术设计选择一样，事务有优点也有局限性。为了理解这些权衡，让我们深入了解事务可以提供的保证的细节——无论是在正常操作中还是在各种极端（但现实）的情况下。

### ACID 的含义 {#sec_transactions_acid}

事务提供的安全保证通常由众所周知的首字母缩略词 *ACID* 来描述，它代表*原子性*（Atomicity）、*一致性*（Consistency）、*隔离性*（Isolation）和*持久性*（Durability）。它由 Theo Härder 和 Andreas Reuter 于 1983 年提出[^9]，旨在为数据库中的容错机制建立精确的术语。

然而，在实践中，一个数据库的 ACID 实现并不等同于另一个数据库的实现。例如，正如我们将看到的，*隔离性*的含义有很多歧义[^10]。高层次的想法是合理的，但魔鬼在细节中。今天，当一个系统声称自己"符合 ACID"时，实际上你能期待什么保证并不清楚。不幸的是，ACID 基本上已经成为了一个营销术语。

（不符合 ACID 标准的系统有时被称为 *BASE*，它代表*基本可用*（Basically Available）、*软状态*（Soft state）和*最终一致性*（Eventual consistency）[^11]。这比 ACID 的定义更加模糊。似乎 BASE 唯一合理的定义是"非 ACID"；即，它几乎可以代表任何你想要的东西。）

让我们深入了解原子性、一致性、隔离性和持久性的定义，这将让我们提炼出事务的思想。

#### 原子性 {#sec_transactions_acid_atomicity}

一般来说，*原子*是指不能分解成更小部分的东西。这个词在计算机的不同分支中意味着相似但又微妙不同的东西。例如，在多线程编程中，如果一个线程执行原子操作，这意味着另一个线程无法看到该操作的半完成结果。系统只能处于操作之前或操作之后的状态，而不是介于两者之间。

相比之下，在 ACID 的上下文中，原子性*不是*关于并发的。它不描述如果几个进程试图同时访问相同的数据会发生什么，因为这包含在字母 *I*（*隔离性*）中（参见["隔离性"](#sec_transactions_acid_isolation)）。

相反，ACID 原子性描述了当客户端想要进行多次写入，但在某些写入被处理后发生故障时会发生什么——例如，进程崩溃、网络连接中断、磁盘变满或违反了某些完整性约束。如果这些写入被分组到一个原子事务中，并且由于故障无法完成（*提交*）事务，则事务被*中止*，数据库必须丢弃或撤消该事务中迄今为止所做的任何写入。

如果没有原子性，如果在进行多处更改的中途发生错误，很难知道哪些更改已经生效，哪些没有。应用程序可以重试，但这有进行两次相同更改的风险，导致数据重复或错误。原子性简化了这个问题：如果事务被中止，应用程序可以确定它没有改变任何东西，因此可以安全地重试。

在错误时中止事务并丢弃该事务的所有写入的能力是 ACID 原子性的定义特征。也许*可中止性*比*原子性*更好，但我们将坚持使用*原子性*，因为这是常用词。

#### 一致性 {#sec_transactions_acid_consistency}

*一致性*这个词被严重滥用：

* 在[第 6 章](/ch6#ch_replication)中，我们讨论了*副本一致性*和异步复制系统中出现的*最终一致性*问题（参见["复制延迟的问题"](/ch6#sec_replication_lag)）。
* 数据库的*一致快照*（例如，用于备份）是整个数据库在某一时刻存在的快照。更准确地说，它与先发生关系（happens-before relation）一致（参见["“先发生”关系和并发"](/ch6#sec_replication_happens_before)）：也就是说，如果快照包含在特定时间写入的值，那么它也反映了在该值写入之前发生的所有写入。
* *一致性哈希*是某些系统用于再平衡的分片方法（参见["一致性哈希"](/ch7#sec_sharding_consistent_hashing)）。
* 在 CAP定理中（参见[第 10 章](/ch10#ch_consistency)），*一致性*一词用于表示*线性一致性*（参见["线性一致性"](/ch10#sec_consistency_linearizability)）。
* 在 ACID 的上下文中，*一致性*是指应用程序特定的数据库处于"良好状态"的概念。

不幸的是，同一个词至少有五种不同的含义。

ACID 一致性的思想是，你对数据有某些陈述（*不变式*）必须始终为真——例如，在会计系统中，所有账户的贷方和借方必须始终平衡。如果事务从满足这些不变式的有效数据库开始，并且事务期间的任何写入都保持有效性，那么你可以确定不变式始终得到满足。（不变式可能在事务执行期间暂时违反，但在事务提交时应该再次满足。）

如果你希望数据库强制执行你的不变式，你需要将它们声明为模式的一部分的*约束*。例如，外键约束、唯一性约束或检查约束（限制单个行中可以出现的值）通常用于对特定类型的不变式建模。更复杂的一致性要求有时可以使用触发器或物化视图建模[^12]。

然而，复杂的不变式可能很难或不可能使用数据库通常提供的约束来建模。在这种情况下，应用程序有责任正确定义其事务，以便它们保持一致性。如果你写入违反不变式的错误数据，但你没有声明这些不变式，数据库无法阻止你。因此，ACID 中的 C 通常取决于应用程序如何使用数据库，而不仅仅是数据库的属性。

#### 隔离性 {#sec_transactions_acid_isolation}

大多数数据库都会同时被多个客户端访问。如果它们读写数据库的不同部分，这没有问题，但如果它们访问相同的数据库记录，你可能会遇到并发问题（竞态条件）。

[图 8-1](#fig_transactions_increment) 是这种问题的一个简单例子。假设你有两个客户端同时递增存储在数据库中的计数器。每个客户端需要读取当前值，加 1，然后写回新值（假设数据库中没有内置的递增操作）。在[图 8-1](#fig_transactions_increment) 中，计数器应该从 42 增加到 44，因为发生了两次递增，但实际上由于竞态条件只增加到 43。

{{< figure src="/fig/ddia_0801.png" id="fig_transactions_increment" caption="图 8-1. 两个客户端并发递增计数器之间的竞态条件。" class="w-full my-4" >}}


ACID 意义上的*隔离性*意味着同时执行的事务彼此隔离：它们不能相互干扰。经典的数据库教科书将隔离性形式化为*可串行化*，这意味着每个事务可以假装它是唯一在整个数据库上运行的事务。数据库确保当事务已经提交时，结果与它们*串行*运行（一个接一个）相同，即使实际上它们可能是并发运行的[^13]。

然而，可串行化有性能成本。在实践中，许多数据库使用比可串行化更弱的隔离形式：也就是说，它们允许并发事务以有限的方式相互干扰。一些流行的数据库，如 Oracle，甚至没有实现它（Oracle 有一个称为"可串行化"的隔离级别，但它实际上实现了*快照隔离*，这是比可串行化更弱的保证[^10] [^14]）。这意味着某些类型的竞态条件仍然可能发生。我们将在["弱隔离级别"](#sec_transactions_isolation_levels)中探讨快照隔离和其他形式的隔离。

#### 持久性 {#durability}

数据库系统的目的是提供一个安全的地方来存储数据，而不用担心丢失它。*持久性*是一个承诺，即一旦事务成功提交，它写入的任何数据都不会被遗忘，即使发生硬件故障或数据库崩溃。

在单节点数据库中，持久性通常意味着数据已经写入非易失性存储，如硬盘或 SSD。定期文件写入通常在发送到磁盘之前在内存中缓冲，这意味着如果突然断电它们将丢失；因此，许多数据库使用 `fsync()` 系统调用来确保数据真正写入磁盘。数据库通常还有预写日志或类似的（参见["使 B 树可靠"](/ch4#sec_storage_btree_wal)），这允许它们在写入过程中发生崩溃时恢复。

在复制数据库中，持久性可能意味着数据已成功复制到某些节点。为了提供持久性保证，数据库必须等到这些写入或复制完成，然后才报告事务成功提交。然而，如["可靠性和容错"](/ch2#sec_introduction_reliability)中所讨论的，完美的持久性不存在：如果所有硬盘和所有备份同时被销毁，显然你的数据库无法挽救你。

--------

<a id="sidebar_transactions_durability"></a>

> [!TIP] 复制与持久性

历史上，持久性意味着写入归档磁带。然后它被理解为写入磁盘或 SSD。最近，它已经适应为意味着复制。哪种实现更好？

事实是，没有什么是完美的：

* 如果你写入磁盘而机器死机，即使你的数据没有丢失，在你修复机器或将磁盘转移到另一台机器之前，它也是不可访问的。复制系统可以保持可用。
* 相关故障——停电或导致每个节点在特定输入上崩溃的错误——可以一次性摧毁所有副本（参见["可靠性和容错"](/ch2#sec_introduction_reliability)），失去任何仅在内存中的数据。因此，写入磁盘对于复制数据库仍然相关。
* 在异步复制系统中，当领导者变得不可用时，最近的写入可能会丢失（参见["处理节点故障"](/ch6#sec_replication_failover)）。
* 当电源突然切断时，SSD 特别被证明有时会违反它们应该提供的保证：即使 `fsync` 也不能保证正常工作[^15]。磁盘固件可能有错误，就像任何其他类型的软件一样[^16] [^17]，例如，导致驱动器在正好 32,768 小时操作后失败[^18]。而且 `fsync` 很难使用；即使 PostgreSQL 使用它不正确超过 20 年[^19] [^20] [^21]。
* 存储引擎和文件系统实现之间的微妙交互可能导致难以追踪的错误，并可能导致磁盘上的文件在崩溃后损坏[^22] [^23]。一个副本上的文件系统错误有时也会传播到其他副本[^24]。
* 磁盘上的数据可能在未被检测到的情况下逐渐损坏[^25]。如果数据已经损坏了一段时间，副本和最近的备份也可能损坏。在这种情况下，你需要尝试从历史备份中恢复数据。
* 一项关于 SSD 的研究发现，在前四年的运行中，30% 到 80% 的驱动器会开发至少一个坏块，其中只有一些可以通过固件纠正[^26]。磁盘驱动器的坏扇区率较低，但完全故障率高于 SSD。
* 当磨损的 SSD（经历了许多写/擦除周期）断电时，它可能在几周到几个月的时间尺度上开始丢失数据，具体取决于温度[^27]。对于磨损水平较低的驱动器，这不是问题[^28]。

在实践中，没有一种技术可以提供绝对保证。只有各种降低风险的技术，包括写入磁盘、复制到远程机器和备份——它们可以而且应该一起使用。一如既往，明智的做法是对任何理论上的"保证"持健康的怀疑态度。

--------

### 单对象与多对象操作 {#sec_transactions_multi_object}

回顾一下，在 ACID 中，原子性和隔离性描述了如果客户端在同一事务中进行多次写入，数据库应该做什么：

原子性
: 如果在写入序列的中途发生错误，事务应该被中止，并且到该点为止所做的写入应该被丢弃。换句话说，数据库让你免于担心部分失败，通过提供全有或全无的保证。

隔离性
: 并发运行的事务不应该相互干扰。例如，如果一个事务进行多次写入，那么另一个事务应该看到所有或不看到这些写入，但不是某些子集。

这些定义假设你想要同时修改多个对象（行、文档、记录）。这种*多对象事务*通常需要保持多块数据同步。[图 8-2](#fig_transactions_read_uncommitted) 显示了一个来自电子邮件应用程序的示例。要显示用户的未读消息数，你可以查询类似这样的内容：

```
SELECT COUNT(*) FROM emails WHERE recipient_id = 2 AND unread_flag = true
```

{{< figure src="/fig/ddia_0802.png" id="fig_transactions_read_uncommitted" caption="图 8-2. 违反隔离性：一个事务读取另一个事务的未提交写入（“脏读”）。" class="w-full my-4" >}}


然而，如果有很多电子邮件，你可能会发现这个查询太慢，并决定将未读消息的数量存储在一个单独的字段中（一种反规范化，我们在["规范化、反规范化和连接"](/ch3#sec_datamodels_normalization)中讨论）。现在，每当有新消息进来时，你必须增加未读计数器，每当消息被标记为已读时，你也必须减少未读计数器。

在[图 8-2](#fig_transactions_read_uncommitted) 中，用户 2 遇到了异常：邮箱列表显示有未读消息，但计数器显示零未读消息，因为计数器增量尚未发生。（如果电子邮件应用程序中的错误计数器看起来太微不足道，请考虑客户账户余额而不是未读计数器，以及支付事务而不是电子邮件。）隔离本可以通过确保用户 2 看到插入的电子邮件和更新的计数器，或者两者都不看到，但不是不一致的中间点，来防止这个问题。

[图 8-3](#fig_transactions_atomicity) 说明了对原子性的需求：如果在事务过程中某处发生错误，邮箱的内容和未读计数器可能会失去同步。在原子事务中，如果对计数器的更新失败，事务将被中止，插入的电子邮件将被回滚。

{{< figure src="/fig/ddia_0803.png" id="fig_transactions_atomicity" caption="图 8-3. 原子性确保如果发生错误，该事务的任何先前写入都会被撤消，以避免不一致的状态。" class="w-full my-4" >}}


多对象事务需要某种方式来确定哪些读写操作属于同一事务。在关系数据库中，这通常基于客户端与数据库服务器的 TCP 连接：在任何特定连接上，`BEGIN TRANSACTION` 和 `COMMIT` 语句之间的所有内容都被认为是同一事务的一部分。如果 TCP 连接中断，事务必须被中止。

另一方面，许多非关系数据库没有这样的方式来将操作组合在一起。即使有多对象 API（例如，键值存储可能有一个*多重放置*操作，在一个操作中更新多个键），这并不一定意味着它具有事务语义：该命令可能在某些键上成功而在其他键上失败，使数据库处于部分更新状态。

#### 单对象写入 {#sec_transactions_single_object}

当单个对象被更改时，原子性和隔离性也适用。例如，假设你正在向数据库写入 20 KB 的 JSON 文档：

* 如果在发送了前 10 KB 后网络连接中断，数据库是否存储了无法解析的 10 KB JSON 片段？
* 如果数据库正在覆盖磁盘上的先前值的过程中电源失效，你是否最终会将新旧值拼接在一起？
* 如果另一个客户端在写入过程中读取该文档，它会看到部分更新的值吗？

这些问题会令人非常困惑，因此存储引擎几乎普遍的目标是在一个节点上的单个对象（如键值对）上提供原子性和隔离性。原子性可以使用日志实现崩溃恢复（参见["使 B 树可靠"](/ch4#sec_storage_btree_wal)），隔离性可以使用每个对象上的锁来实现（一次只允许一个线程访问对象）。

某些数据库还提供更复杂的原子操作，例如递增操作，它消除了像[图 8-1](#fig_transactions_increment) 中那样的读-修改-写循环的需求。类似流行的是*条件写入*操作，它允许仅在值未被其他人并发更改时才进行写入（参见["条件写入（比较并设置）"](#sec_transactions_compare_and_set)），类似于共享内存并发中的比较并设置或比较并交换（CAS）操作。

--------

> [!NOTE]
> 严格来说，术语*原子递增*在多线程编程的意义上使用了*原子*这个词。在 ACID 的上下文中，它实际上应该被称为*隔离*或*可串行化*递增，但这不是通常的术语。

--------

这些单对象操作很有用，因为它们可以防止多个客户端尝试同时写入同一对象时的丢失更新（参见["防止丢失更新"](#sec_transactions_lost_update)）。然而，它们不是通常意义上的事务。例如，Cassandra 和 ScyllaDB 的"轻量级事务"功能以及 Aerospike 的"强一致性"模式在单个对象上提供线性一致（参见["线性一致性"](/ch10#sec_consistency_linearizability)）读取和条件写入，但不保证跨多个对象。

#### 多对象事务的需求 {#sec_transactions_need}

我们是否需要多对象事务？是否可能仅使用键值数据模型和单对象操作来实现任何应用程序？

在某些用例中，单对象插入、更新和删除就足够了。然而，在许多其他情况下，需要协调对多个不同对象的写入：

* 在关系数据模型中，一个表中的行通常具有对另一个表中行的外键引用。类似地，在类似图的数据模型中，顶点具有指向其他顶点的边。多对象事务允许你确保这些引用保持有效：插入引用彼此的多个记录时，外键必须正确且最新，否则数据变得毫无意义。
* 在文档数据模型中，需要一起更新的字段通常在同一文档内，它被视为单个对象——更新单个文档时不需要多对象事务。然而，缺乏连接功能的文档数据库也鼓励反规范化（参见["何时使用哪种模型"](/ch3#sec_datamodels_document_summary)）。当需要更新反规范化信息时，如[图 8-2](#fig_transactions_read_uncommitted) 的示例，你需要一次更新多个文档。事务在这种情况下非常有用，可以防止反规范化数据失去同步。
* 在具有二级索引的数据库中（几乎除了纯键值存储之外的所有数据库），每次更改值时都需要更新索引。从事务的角度来看，这些索引是不同的数据库对象：例如，如果没有事务隔离，记录可能出现在一个索引中但不在另一个索引中，因为对第二个索引的更新尚未发生（参见["分片和二级索引"](/ch7#sec_sharding_secondary_indexes)）。

这些应用程序仍然可以在没有事务的情况下实现。然而，没有原子性的错误处理变得更加复杂，缺乏隔离性可能导致并发问题。我们将在["弱隔离级别"](#sec_transactions_isolation_levels)中讨论这些问题，并在["派生数据与分布式事务"](/ch13#sec_future_derived_vs_transactions)中探索替代方法。

#### 处理错误和中止 {#handling-errors-and-aborts}

事务的一个关键特性是，如果发生错误，它可以被中止并安全地重试。ACID 数据库基于这样的哲学：如果数据库有违反其原子性、隔离性或持久性保证的危险，它宁愿完全放弃事务，也不允许它保持半完成状态。

然而，并非所有系统都遵循这种哲学。特别是，具有无主（无领导者）复制的数据存储（参见["无主（无领导者）复制"](/ch6#sec_replication_leaderless)）更多地基于"尽力而为"的基础工作，可以总结为"数据库将尽其所能，如果遇到错误，它不会撤消已经完成的操作"——因此，从错误中恢复是应用程序的责任。

错误不可避免地会发生，但许多软件开发人员更愿意只考虑快乐路径，而不是错误处理的复杂性。例如，流行的对象关系映射（ORM）框架，如 Rails 的 ActiveRecord 和 Django，不会重试中止的事务——错误通常导致异常冒泡到堆栈中，因此任何用户输入都被丢弃，用户收到错误消息。这是一种遗憾，因为中止的全部意义是启用安全重试。

尽管重试中止的事务是一种简单有效的错误处理机制，但它并不完美：

* 如果事务实际上成功了，但在服务器尝试向客户端确认成功提交时网络中断（因此从客户端的角度来看超时），那么重试事务会导致它被执行两次——除非你有额外的应用程序级去重机制。
* 如果错误是由于过载或并发事务之间的高争用，重试事务会使问题变得更糟，而不是更好。为了避免这种反馈循环，你可以限制重试次数，使用指数退避，并以不同的方式处理与过载相关的错误与其他错误（参见["当过载系统无法恢复时"](/ch2#sidebar_metastable)）。
* 仅在瞬态错误后重试才值得（例如，由于死锁、隔离违规、临时网络中断和故障转移）；在永久错误后（例如，约束违规）重试将毫无意义。
* 如果事务在数据库之外也有副作用，即使事务被中止，这些副作用也可能发生。例如，如果你正在发送电子邮件，你不会希望每次重试事务时都再次发送电子邮件。如果你想确保几个不同的系统一起提交或中止，两阶段提交可以提供帮助（我们将在["两阶段提交（2PC）"](#sec_transactions_2pc)中讨论这个问题）。
* 如果客户端进程在重试时崩溃，它试图写入数据库的任何数据都会丢失。



## 弱隔离级别 {#sec_transactions_isolation_levels}

如果两个事务不访问相同的数据，或者都是只读的，它们可以安全地并行运行，因为它们互不依赖。仅当一个事务读取另一个事务并发修改的数据时，或者当两个事务尝试同时修改相同的数据时，才会出现并发问题（竞态条件）。

并发错误很难通过测试发现，因为这些错误只有在时机不巧时才会触发。这种时机问题可能非常罕见，通常难以重现。并发也很难推理，特别是在大型应用程序中，你不一定知道代码的其他部分正在访问数据库。如果只有一个用户，应用程序开发就已经够困难了；有许多并发用户会让情况变得更加困难，因为任何数据都可能在任何时候意外地发生变化。

出于这个原因，数据库长期以来一直试图通过提供*事务隔离*来向应用程序开发人员隐藏并发问题。理论上，隔离应该让你的生活更轻松，让你假装没有并发发生：*可串行化*隔离意味着数据库保证事务具有与*串行*运行（即一次一个，没有任何并发）相同的效果。

在实践中，隔离不幸并不那么简单。可串行化隔离有性能成本，许多数据库不愿意支付这个代价[^10]。因此，系统通常使用较弱的隔离级别，这些级别可以防止*某些*并发问题，但不是全部。这些隔离级别更难理解，它们可能导致微妙的错误，但它们在实践中仍然被使用[^29]。

由弱事务隔离引起的并发错误不仅仅是理论问题。它们已经导致了巨额资金损失[^30] [^31] [^32]，引发了金融审计师的调查[^33]，并导致客户数据损坏[^34]。对此类问题披露的一个流行评论是"如果你正在处理金融数据，请使用 ACID 数据库！"——但这没有抓住重点。即使许多流行的关系数据库系统（通常被认为是"ACID"）使用弱隔离，因此它们不一定能防止这些错误发生。

--------

> [!NOTE]
> 顺便说一句，银行系统的大部分依赖于通过安全 FTP 交换的文本文件[^35]。在这种情况下，拥有审计跟踪和一些人为级别的欺诈预防措施实际上比 ACID 属性更重要。

--------

这些例子还强调了一个重要观点：即使并发问题在正常操作中很少见，你也必须考虑攻击者故意向你的 API 发送大量高度并发请求以故意利用并发错误的可能性[^30]。因此，为了构建可靠和安全的应用程序，你必须确保系统地防止此类错误。

在本节中，我们将研究实践中使用的几种弱（非可串行化）隔离级别，并详细讨论哪些竞态条件可以发生和不能发生，以便你可以决定哪个级别适合你的应用程序。完成后，我们将详细讨论可串行化（参见["可串行化"](#sec_transactions_serializability)）。我们对隔离级别的讨论将是非正式的，使用示例。如果你想要严格的定义和对其属性的分析，你可以在学术文献中找到它们[^36] [^37] [^38] [^39]。

### 读已提交 {#sec_transactions_read_committed}

最基本的事务隔离级别是*读已提交*。它提供两个保证：

1. 从数据库读取时，你只会看到已经提交的数据（没有*脏读*）。
2. 写入数据库时，你只会覆盖已经提交的数据（没有*脏写*）。

某些数据库支持更弱的隔离级别，称为*读未提交*。它防止脏写，但不防止脏读。让我们更详细地讨论这两个保证。

#### 没有脏读 {#no-dirty-reads}

想象一个事务已经向数据库写入了一些数据，但事务尚未提交或中止。另一个事务能看到那个未提交的数据吗？如果能，这称为*脏读*[^3]。

在读已提交隔离级别下运行的事务必须防止脏读。这意味着事务的任何写入只有在该事务提交时才对其他人可见（然后它的所有写入立即变得可见）。这在[图 8-4](#fig_transactions_read_committed) 中说明，其中用户 1 已设置 *x* = 3，但用户 2 的 *get x* 仍返回旧值 2，因为用户 1 尚未提交。

{{< figure src="/fig/ddia_0804.png" id="fig_transactions_read_committed" caption="图 8-4. 没有脏读：用户 2 只有在用户 1 的事务提交后才能看到 x 的新值。" class="w-full my-4" >}}

有几个原因说明为什么防止脏读是有用的：

* 如果事务需要更新多行，脏读意味着另一个事务可能看到某些更新但不是其他更新。例如，在[图 8-2](#fig_transactions_read_uncommitted) 中，用户看到新的未读电子邮件但没有看到更新的计数器。这是电子邮件的脏读。看到数据库处于部分更新状态会让用户感到困惑，并可能导致其他事务做出错误的决定。
* 如果事务中止，它所做的任何写入都需要回滚（如[图 8-3](#fig_transactions_atomicity)）。如果数据库允许脏读，这意味着事务可能看到后来被回滚的数据——即从未实际提交到数据库的数据。任何读取未提交数据的事务也需要被中止，导致称为*级联中止*的问题。

#### 没有脏写 {#sec_transactions_dirty_write}

如果两个事务并发尝试更新数据库中的同一行会发生什么？我们不知道写入将以什么顺序发生，但我们通常假设后面的写入会覆盖前面的写入。

然而，如果前面的写入是尚未提交的事务的一部分，因此后面的写入覆盖了一个未提交的值，会发生什么？这称为*脏写*[^36]。在读已提交隔离级别下运行的事务必须防止脏写，通常通过延迟第二个写入直到第一个写入的事务已提交或中止。

通过防止脏写，这个隔离级别避免了某些类型的并发问题：

* 如果事务更新多行，脏写可能导致糟糕的结果。例如，考虑[图 8-5](#fig_transactions_dirty_writes)，它说明了一个二手车销售网站，两个人 Aaliyah 和 Bryce 同时尝试购买同一辆车。购买汽车需要两次数据库写入：网站上的列表需要更新以反映买家，销售发票需要发送给买家。在[图 8-5](#fig_transactions_dirty_writes) 的情况下，销售被授予 Bryce（因为他对 `listings` 表执行了获胜的更新），但发票被发送给 Aaliyah（因为她对 `invoices` 表执行了获胜的更新）。读已提交防止了这种事故。
* 然而，读已提交*不*防止[图 8-1](#fig_transactions_increment) 中两个计数器递增之间的竞态条件。在这种情况下，第二个写入发生在第一个事务提交之后，所以它不是脏写。它仍然是不正确的，但原因不同——在["防止丢失更新"](#sec_transactions_lost_update)中，我们将讨论如何使此类计数器递增安全。

{{< figure src="/fig/ddia_0805.png" id="fig_transactions_dirty_writes" caption="图 8-5. 有了脏写，来自不同事务的冲突写入可能会混在一起。" class="w-full my-4" >}}


#### 实现读已提交 {#sec_transactions_read_committed_impl}

读已提交是一个非常流行的隔离级别。它是 Oracle Database、PostgreSQL、SQL Server 和许多其他数据库中的默认设置[^10]。

最常见的是，数据库通过使用行级锁来防止脏写：当事务想要修改特定行（或文档或其他对象）时，它必须首先获取该行的锁。然后它必须持有该锁直到事务提交或中止。任何给定行只能有一个事务持有锁；如果另一个事务想要写入同一行，它必须等到第一个事务提交或中止后才能获取锁并继续。这种锁定由数据库在读已提交模式（或更强的隔离级别）下自动完成。

我们如何防止脏读？一种选择是使用相同的锁，并要求任何想要读取行的事务短暂地获取锁，然后在读取后立即再次释放它。这将确保在行具有脏的、未提交的值时无法进行读取（因为在此期间锁将由进行写入的事务持有）。

然而，要求读锁的方法在实践中效果不佳，因为一个长时间运行的写事务可以强制许多其他事务等待，直到长时间运行的事务完成，即使其他事务只读取并且不向数据库写入任何内容。这会损害只读事务的响应时间，并且对可操作性不利：应用程序一个部分的减速可能会由于等待锁而在应用程序的完全不同部分产生连锁效应。

尽管如此，在某些数据库中使用锁来防止脏读，例如 IBM Db2 和 Microsoft SQL Server 在 `read_committed_snapshot=off` 设置中[^29]。

防止脏读的更常用方法是[图 8-4](#fig_transactions_read_committed) 中说明的方法：对于每个被写入的行，数据库记住旧的已提交值和当前持有写锁的事务设置的新值。当事务正在进行时，任何其他读取该行的事务都只是被给予旧值。只有当新值被提交时，事务才会切换到读取新值（有关更多详细信息，请参见["多版本并发控制（MVCC）"](#sec_transactions_snapshot_impl)）。

### 快照隔离与可重复读 {#sec_transactions_snapshot_isolation}

如果你肤浅地看待读已提交隔离，你可能会被原谅认为它做了事务需要做的一切：它允许中止（原子性所需），它防止读取事务的不完整结果，并且它防止并发写入混淆。确实，这些是有用的功能，比没有事务的系统能获得的保证要强得多。

然而，使用这个隔离级别时，仍然有很多方式可能出现并发错误。例如，[图 8-6](#fig_transactions_item_many_preceders) 说明了读已提交可能发生的问题。

{{< figure src="/fig/ddia_0806.png" id="fig_transactions_item_many_preceders" caption="图 8-6. 读取偏差：Aaliyah 观察到数据库处于不一致状态。" class="w-full my-4" >}}


假设 Aaliyah 在银行有 1,000 美元的储蓄，分成两个账户，每个 500 美元。现在一笔事务从她的一个账户转账 100 美元到另一个账户。如果她不幸在该事务处理的同时查看她的账户余额列表，她可能会看到一个账户余额在收款到达之前（余额为 500 美元），另一个账户在转出之后（新余额为 400 美元）。对 Aaliyah 来说，现在她的账户总共只有 900 美元——似乎 100 美元凭空消失了。

这种异常称为*读取偏差*，它是*不可重复读*的一个例子：如果 Aaliyah 在事务结束时再次读取账户 1 的余额，她会看到与之前查询中看到的不同的值（600 美元）。读取偏差在读已提交隔离下被认为是可接受的：Aaliyah 看到的账户余额确实是在她读取它们时已提交的。

--------

> [!NOTE]
> 术语*偏斜*不幸地被重载了：我们之前在*具有热点的不平衡工作负载*的意义上使用它（参见["倾斜负载和缓解热点"](/ch7#sec_sharding_skew)），而这里它意味着*时序异常*。

--------

在 Aaliyah 的情况下，这不是一个持久的问题，因为如果她几秒钟后重新加载在线银行网站，她很可能会看到一致的账户余额。然而，某些情况不能容忍这种临时的不一致性：

备份
: 进行备份需要复制整个数据库，对于大型数据库可能需要几个小时。在备份过程运行期间，写入将继续对数据库进行。因此，你最终可能会得到备份的某些部分包含较旧版本的数据，而其他部分包含较新版本。如果你需要从这样的备份恢复，不一致性（如消失的钱）将变成永久性的。

分析查询和完整性检查
: 有时，你可能想要运行扫描数据库大部分的查询。此类查询在分析中很常见（参见["分析与运营系统"](/ch1#sec_introduction_analytics)），或者可能是定期完整性检查的一部分，以确保一切正常（监控数据损坏）。如果这些查询在不同时间点观察数据库的不同部分，它们很可能返回无意义的结果。

*快照隔离*[^36] 是解决这个问题的最常见方法。其思想是每个事务从数据库的*一致快照*读取——也就是说，事务看到事务开始时数据库中已提交的所有数据。即使数据随后被另一个事务更改，每个事务也只能看到该特定时间点的旧数据。

快照隔离对于长时间运行的只读查询（如备份和分析）来说是一个福音。如果查询操作的数据在查询执行的同时发生变化，很难推理查询的含义。当事务可以看到数据库的一致快照（冻结在特定时间点）时，理解起来就容易得多。

快照隔离是一个流行的功能：它的变体受到 PostgreSQL、使用 InnoDB 存储引擎的 MySQL、Oracle、SQL Server 等的支持，尽管详细行为因系统而异[^29] [^40] [^41]。某些数据库，如 Oracle、TiDB 和 Aurora DSQL，甚至选择快照隔离作为它们的最高隔离级别。

#### 多版本并发控制（MVCC） {#sec_transactions_snapshot_impl}

与读已提交隔离一样，快照隔离的实现通常使用写锁来防止脏写（参见["实现读已提交"](#sec_transactions_read_committed_impl)），这意味着进行写入的事务可以阻止写入同一行的另一个事务的进度。但是，读取不需要任何锁。从性能的角度来看，快照隔离的一个关键原则是*读者永远不会阻塞写者，写者永远不会阻塞读者*。这允许数据库在一致快照上处理长时间运行的读查询，同时正常处理写入，两者之间没有任何锁争用。

为了实现快照隔离，数据库使用了我们在[图 8-4](#fig_transactions_read_committed) 中看到的防止脏读机制的泛化。数据库必须潜在地保留每行的几个不同的已提交版本，而不是每行的两个版本（已提交版本和被覆盖但尚未提交的版本），因为各种正在进行的事务可能需要在不同时间点看到数据库的状态。因为它并排维护一行的多个版本，所以这种技术被称为*多版本并发控制*（MVCC）。

[图 8-7](#fig_transactions_mvcc) 说明了 PostgreSQL 中如何实现基于 MVCC 的快照隔离[^40] [^42] [^43]（其他实现类似）。当事务启动时，它被赋予一个唯一的、始终递增的事务 ID（`txid`）。每当事务向数据库写入任何内容时，它写入的数据都用写入者的事务 ID 标记。（准确地说，PostgreSQL 中的事务 ID 是 32 位整数，因此它们在大约 40 亿个事务后溢出。清理过程执行清理以确保溢出不会影响数据。）

{{< figure src="/fig/ddia_0807.png" id="fig_transactions_mvcc" caption="图 8-7. 使用多版本并发控制实现快照隔离。" class="w-full my-4" >}}


表中的每一行都有一个 `inserted_by` 字段，包含将此行插入表中的事务的 ID。此外，每行都有一个 `deleted_by` 字段，最初为空。如果事务删除一行，该行实际上不会从数据库中删除，而是通过将 `deleted_by` 字段设置为请求删除的事务的 ID 来标记为删除。在稍后的某个时间，当确定没有事务可以再访问已删除的数据时，数据库中的垃圾收集过程会删除任何标记为删除的行并释放它们的空间。

更新在内部被转换为删除和插入[^44]。例如，在[图 8-7](#fig_transactions_mvcc) 中，事务 13 从账户 2 中扣除 100 美元，将余额从 500 美元更改为 400 美元。`accounts` 表现在实际上包含账户 2 的两行：余额为 500 美元的行被事务 13 标记为已删除，余额为 400 美元的行由事务 13 插入。

行的所有版本都存储在同一个数据库堆中（参见["在索引中存储值"](/ch4#sec_storage_index_heap)），无论写入它们的事务是否已提交。同一行的版本形成一个链表，从最新版本到最旧版本或相反，以便查询可以在内部迭代行的所有版本[^45] [^46]。

#### 观察一致快照的可见性规则 {#sec_transactions_mvcc_visibility}

当事务从数据库读取时，事务 ID 用于决定它可以看到哪些行版本以及哪些是不可见的。通过仔细定义可见性规则，数据库可以向应用程序呈现数据库的一致快照。这大致如下工作[^43]：

1. 在每个事务开始时，数据库列出当时正在进行（尚未提交或中止）的所有其他事务。这些事务所做的任何写入都被忽略，即使事务随后提交。这确保我们看到一个不受另一个事务提交影响的一致快照。
2. 具有较晚事务 ID（即在当前事务开始后开始，因此不包括在正在进行的事务列表中）的事务所做的任何写入都被忽略，无论这些事务是否已提交。
3. 中止事务所做的任何写入都被忽略，无论该中止何时发生。这样做的好处是，当事务中止时，我们不需要立即从存储中删除它写入的行，因为可见性规则会将它们过滤掉。垃圾收集过程可以稍后删除它们。
4. 所有其他写入对应用程序的查询可见。

这些规则适用于行的插入和删除。在[图 8-7](#fig_transactions_mvcc) 中，当事务 12 从账户 2 读取时，它看到 500 美元的余额，因为 500 美元余额的删除是由事务 13 进行的（根据规则 2，事务 12 无法看到事务 13 进行的删除），而 400 美元余额的插入尚不可见（根据相同的规则）。

换句话说，如果以下两个条件都为真，则行是可见的：

* 在读者事务开始时，插入该行的事务已经提交。
* 该行未标记为删除，或者如果是，请求删除的事务在读者事务开始时尚未提交。

长时间运行的事务可能会长时间继续使用快照，继续读取（从其他事务的角度来看）早已被覆盖或删除的值。通过永远不更新原地的值，而是在每次更改值时插入新版本，数据库可以提供一致的快照，同时只产生很小的开销。

<a id="sec_transactions_snapshot_indexes"></a>

#### 索引与快照隔离 {#indexes-and-snapshot-isolation}

索引如何在多版本数据库中工作？最常见的方法是每个索引条目指向与该条目匹配的行的一个版本（最旧或最新版本）。每个行版本可能包含对下一个最旧或下一个最新版本的引用。使用索引的查询必须迭代行以找到可见的行，并且值与查询要查找的内容匹配。当垃圾收集删除不再对任何事务可见的旧行版本时，相应的索引条目也可以被删除。

许多实现细节影响多版本并发控制的性能[^45] [^46]。例如，如果同一行的不同版本可以适合同一页面，PostgreSQL 有避免索引更新的优化[^40]。其他一些数据库避免存储修改行的完整副本，而只存储版本之间的差异以节省空间。

CouchDB、Datomic 和 LMDB 使用另一种方法。尽管它们也使用 B 树（参见["B 树"](/ch4#sec_storage_b_trees)），但它们使用*不可变*（写时复制）变体，在更新时不会覆盖树的页面，而是创建每个修改页面的新副本。父页面，直到树的根，被复制并更新以指向其子页面的新版本。任何不受写入影响的页面都不需要复制，并且可以与新树共享[^47]。

使用不可变 B 树，每个写事务（或事务批次）都会创建一个新的 B 树根，特定的根是创建时数据库的一致快照。不需要基于事务 ID 过滤行，因为后续写入无法修改现有的 B 树；它们只能创建新的树根。这种方法还需要后台进程进行压缩和垃圾收集。

#### 快照隔离、可重复读和命名混淆 {#snapshot-isolation-repeatable-read-and-naming-confusion}

MVCC 是数据库常用的实现技术，通常用于实现快照隔离。然而，不同的数据库有时使用不同的术语来指代同一件事：例如，快照隔离在 PostgreSQL 中称为"可重复读"，在 Oracle 中称为"可串行化"[^29]。有时不同的系统使用相同的术语来表示不同的东西：例如，虽然在 PostgreSQL 中"可重复读"意味着快照隔离，但在 MySQL 中它意味着比快照隔离更弱一致性的 MVCC 实现[^41]。

这种命名混淆的原因是 SQL 标准没有快照隔离的概念，因为该标准基于 System R 1975 年的隔离级别定义[^3]，而快照隔离当时还没有被发明。相反，它定义了可重复读，表面上看起来类似于快照隔离。PostgreSQL 将其快照隔离级别称为"可重复读"，因为它符合标准的要求，因此他们可以声称符合标准。

不幸的是，SQL 标准对隔离级别的定义是有缺陷的——它是模糊的、不精确的，并且不像标准应该的那样独立于实现[^36]。即使几个数据库实现了可重复读，它们实际提供的保证也有很大差异，尽管表面上是标准化的[^29]。研究文献中有可重复读的正式定义[^37] [^38]，但大多数实现不满足该正式定义。最重要的是，IBM Db2 使用"可重复读"来指代可串行化[^10]。

因此，没有人真正知道可重复读意味着什么。

### 防止丢失更新 {#sec_transactions_lost_update}

到目前为止，我们讨论的读已提交和快照隔离级别主要是关于只读事务在并发写入存在的情况下可以看到什么的保证。我们大多忽略了两个事务并发写入的问题——我们只讨论了脏写（参见["没有脏写"](#sec_transactions_dirty_write)），这是可能发生的一种特定类型的写-写冲突。

并发写入事务之间还可能发生其他几种有趣的冲突。其中最著名的是*丢失更新*问题，在[图 8-1](#fig_transactions_increment) 中以两个并发计数器递增的例子说明。

如果应用程序从数据库读取某个值，修改它，然后写回修改后的值（*读-修改-写循环*），就会出现丢失更新问题。如果两个事务并发执行此操作，其中一个修改可能会丢失，因为第二个写入不包括第一个修改。（我们有时说后面的写入*覆盖*了前面的写入。）这种模式出现在各种不同的场景中：

* 递增计数器或更新账户余额（需要读取当前值，计算新值，并写回更新的值）
* 对复杂值进行本地更改，例如，向 JSON 文档中的列表添加元素（需要解析文档，进行更改，并写回修改后的文档）
* 两个用户同时编辑 wiki 页面，每个用户通过将整个页面内容发送到服务器来保存他们的更改，覆盖数据库中当前的任何内容

因为这是一个如此常见的问题，已经开发了各种解决方案[^48]。

#### 原子写操作 {#atomic-write-operations}

许多数据库提供原子更新操作，消除了在应用程序代码中实现读-修改-写循环的需要。如果你的代码可以用这些操作来表达，它们通常是最好的解决方案。例如，以下指令在大多数关系数据库中是并发安全的：

```sql
UPDATE counters SET value = value + 1 WHERE key = 'foo';
```

类似地，文档数据库（如 MongoDB）提供原子操作来对 JSON 文档的一部分进行本地修改，Redis 提供原子操作来修改数据结构（如优先级队列）。并非所有写入都可以轻松地用原子操作来表达——例如，对 wiki 页面的更新涉及任意文本编辑，可以使用["CRDT 和操作转换"](/ch6#sec_replication_crdts)中讨论的算法来处理——但在可以使用原子操作的情况下，它们通常是最佳选择。

原子操作通常通过在读取对象时对其进行独占锁来实现，以便在应用更新之前没有其他事务可以读取它。另一种选择是简单地强制所有原子操作在单个线程上执行。

不幸的是，对象关系映射（ORM）框架很容易意外地编写执行不安全的读-修改-写循环的代码，而不是使用数据库提供的原子操作[^49] [^50] [^51]。这可能是难以通过测试发现的微妙错误的来源。

#### 显式锁定 {#explicit-locking}

如果数据库的内置原子操作不提供必要的功能，另一个防止丢失更新的选项是应用程序显式锁定要更新的对象。然后应用程序可以执行读-修改-写循环，如果任何其他事务尝试并发更新或锁定同一对象，它将被迫等到第一个读-修改-写循环完成。

例如，考虑一个多人游戏，其中几个玩家可以同时移动同一个棋子。在这种情况下，原子操作可能不够，因为应用程序还需要确保玩家的移动遵守游戏规则，这涉及一些你无法合理地作为数据库查询实现的逻辑。相反，你可以使用锁来防止两个玩家同时移动同一个棋子，如[例 8-1](#fig_transactions_select_for_update) 所示。

{{< figure id="fig_transactions_select_for_update" title="例 8-1. 显式锁定行以防止丢失更新" class="w-full my-4" >}}

```sql
BEGIN TRANSACTION;

SELECT * FROM figures
    WHERE name = 'robot' AND game_id = 222
    FOR UPDATE; ❶

-- 检查移动是否有效，然后更新
-- 前一个 SELECT 返回的棋子的位置。
UPDATE figures SET position = 'c4' WHERE id = 1234;

COMMIT;
```

❶：`FOR UPDATE` 子句表示数据库应该对此查询返回的所有行进行锁定。

这是有效的，但要正确执行，你需要仔细考虑你的应用程序逻辑。很容易忘记在代码中的某个地方添加必要的锁，从而引入竞态条件。

此外，如果你锁定多个对象，则存在死锁的风险，其中两个或多个事务正在等待彼此释放锁。许多数据库会自动检测死锁，并中止涉及的事务之一，以便系统可以取得进展。你可以在应用程序级别通过重试中止的事务来处理这种情况。

#### 自动检测丢失的更新 {#automatically-detecting-lost-updates}

原子操作和锁是通过强制读-修改-写循环按顺序发生来防止丢失更新的方法。另一种选择是允许它们并行执行，如果事务管理器检测到丢失的更新，则中止事务并强制它重试其读-修改-写循环。

这种方法的一个优点是数据库可以与快照隔离一起有效地执行此检查。实际上，PostgreSQL 的可重复读、Oracle 的可串行化和 SQL Server 的快照隔离级别会自动检测何时发生丢失的更新并中止有问题的事务。然而，MySQL/InnoDB 的可重复读不检测丢失的更新[^29] [^41]。一些作者[^36] [^38] 认为数据库必须防止丢失的更新才能提供快照隔离，因此根据这个定义，MySQL 不提供快照隔离。

丢失更新检测是一个很好的功能，因为它不需要应用程序代码使用任何特殊的数据库功能——你可能忘记使用锁或原子操作从而引入错误，但丢失更新检测会自动发生，因此不太容易出错。但是，你还必须在应用程序级别重试中止的事务。

#### 条件写入（比较并设置） {#sec_transactions_compare_and_set}

在不提供事务的数据库中，你有时会发现一个*条件写入*操作，它可以通过仅在值自你上次读取以来未更改时才允许更新来防止丢失的更新（之前在["单对象写入"](#sec_transactions_single_object)中提到）。如果当前值与你之前读取的不匹配，则更新无效，必须重试读-修改-写循环。它是许多 CPU 支持的原子*比较并设置*或*比较并交换*（CAS）指令的数据库等价物。

例如，为了防止两个用户同时更新同一个 wiki 页面，你可以尝试类似这样的操作，期望仅当页面内容自用户开始编辑以来没有更改时才进行更新：

```sql
-- 这可能安全也可能不安全，取决于数据库实现
UPDATE wiki_pages SET content = 'new content'
    WHERE id = 1234 AND content = 'old content';
```

如果内容已更改并且不再匹配 `'old content'`，则此更新将无效，因此你需要检查更新是否生效并在必要时重试。你也可以使用在每次更新时递增的版本号列，并且仅在当前版本号未更改时才应用更新，而不是比较完整内容。这种方法有时称为*乐观锁定*[^52]。

请注意，如果另一个事务并发修改了 `content`，则根据 MVCC 可见性规则，新内容可能不可见（参见["观察一致快照的可见性规则"](#sec_transactions_mvcc_visibility)）。MVCC 的许多实现对此场景有可见性规则的例外，其中其他事务写入的值对 `UPDATE` 和 `DELETE` 查询的 `WHERE` 子句的评估可见，即使这些写入在快照中不可见。

#### 冲突解决与复制 {#conflict-resolution-and-replication}

在复制数据库中（参见[第 6 章](/ch6#ch_replication)），防止丢失的更新具有另一个维度：由于它们在多个节点上有数据副本，并且数据可能在不同节点上并发修改，因此需要采取一些额外的步骤来防止丢失的更新。

锁和条件写入操作假设有一个最新的数据副本。然而，具有多领导者或无主（无领导者）复制的数据库通常允许多个写入并发发生并异步复制它们，因此它们不能保证有一个最新的数据副本。因此，基于锁或条件写入的技术在此上下文中不适用。（我们将在["线性一致性"](/ch10#sec_consistency_linearizability)中更详细地重新讨论这个问题。）

相反，如["处理冲突写入"](/ch6#sec_replication_write_conflicts)中所讨论的，此类复制数据库中的常见方法是允许并发写入创建值的多个冲突版本（也称为*兄弟节点*），并使用应用程序代码或特殊数据结构在事后解决和合并这些版本。

如果更新是可交换的（即，你可以在不同副本上以不同顺序应用它们，仍然得到相同的结果），合并冲突值可以防止丢失的更新。例如，递增计数器或向集合添加元素是可交换操作。这就是 CRDT 背后的想法，我们在["CRDT 和操作转换"](/ch6#sec_replication_crdts)中遇到过。然而，某些操作（如条件写入）不能成为可交换的。

另一方面，*最后写入胜利*（LWW）冲突解决方法容易丢失更新，如["最后写入胜利（丢弃并发写入）"](/ch6#sec_replication_lww)中所讨论的。不幸的是，LWW 是许多复制数据库中的默认值。

### 写偏差与幻读 {#sec_transactions_write_skew}

在前面的部分中，我们看到了*脏写*和*丢失更新*，这是当不同事务并发尝试写入相同对象时可能发生的两种竞态条件。为了避免数据损坏，需要防止这些竞态条件——要么由数据库自动防止，要么通过使用锁或原子写操作等手动保护措施。

然而，这并不是并发写入之间可能发生的潜在竞态条件列表的结尾。在本节中，我们将看到一些更微妙的冲突示例。

首先，想象这个例子：你正在为医生编写一个应用程序来管理他们在医院的值班班次。医院通常试图在任何时候都有几位医生值班，但绝对必须至少有一位医生值班。医生可以放弃他们的班次（例如，如果他们自己生病了），前提是该班次中至少有一位同事留在值班[^53] [^54]。

现在想象 Aaliyah 和 Bryce 是特定班次的两位值班医生。两人都感觉不舒服，所以他们都决定请假。不幸的是，他们碰巧大约在同一时间点击了下班的按钮。接下来发生的事情如[图 8-8](#fig_transactions_write_skew) 所示。

{{< figure src="/fig/ddia_0808.png" id="fig_transactions_write_skew" caption="图 8-8. 写偏差导致应用程序错误的示例。" class="w-full my-4" >}}


在每个事务中，你的应用程序首先检查当前是否有两个或更多医生在值班；如果是，它假设一个医生下班是安全的。由于数据库使用快照隔离，两个检查都返回 `2`，因此两个事务都继续到下一阶段。Aaliyah 更新她自己的记录让自己下班，Bryce 同样更新他自己的记录。两个事务都提交，现在没有医生值班。你至少有一个医生值班的要求被违反了。

#### 写偏差的特征 {#characterizing-write-skew}

这种异常称为*写偏差*[^36]。它既不是脏写也不是丢失的更新，因为两个事务正在更新两个不同的对象（分别是 Aaliyah 和 Bryce 的值班记录）。这里发生冲突不太明显，但这绝对是一个竞态条件：如果两个事务一个接一个地运行，第二个医生将被阻止下班。异常行为只有在事务并发运行时才可能。

你可以将写偏差视为丢失更新问题的概括。如果两个事务读取相同的对象，然后更新其中一些对象（不同的事务可能更新不同的对象），就会发生写偏差。在不同事务更新同一对象的特殊情况下，你会得到脏写或丢失更新异常（取决于时机）。

我们看到有各种不同的方法可以防止丢失的更新。对于写偏差，我们的选择更受限制：

* 原子单对象操作没有帮助，因为涉及多个对象。
* 不幸的是，你在某些快照隔离实现中发现的丢失更新的自动检测也没有帮助：写偏差在 PostgreSQL 的可重复读、MySQL/InnoDB 的可重复读、Oracle 的可串行化或 SQL Server 的快照隔离级别中不会自动检测到[^29]。自动防止写偏差需要真正的可串行化隔离（参见["可串行化"](#sec_transactions_serializability)）。
* 某些数据库允许你配置约束，然后由数据库强制执行（例如，唯一性、外键约束或对特定值的限制）。但是，为了指定至少有一个医生必须值班，你需要一个涉及多个对象的约束。大多数数据库没有对此类约束的内置支持，但你可能能够使用触发器或物化视图实现它们，如["一致性"](#sec_transactions_acid_consistency)中所讨论的[^12]。
* 如果你不能使用可串行化隔离级别，在这种情况下，第二好的选择可能是显式锁定事务所依赖的行。在医生示例中，你可以编写如下内容：

    ```sql
    BEGIN TRANSACTION;

    SELECT * FROM doctors
        WHERE on_call = true
        AND shift_id = 1234 FOR UPDATE; ❶

    UPDATE doctors
       SET on_call = false
       WHERE name = 'Aaliyah'
       AND shift_id = 1234;

    COMMIT;
    ```

❶：和以前一样，`FOR UPDATE` 告诉数据库锁定此查询返回的所有行。

#### 写偏差的更多例子 {#more-examples-of-write-skew}

写偏差起初可能看起来是一个深奥的问题，但一旦你意识到它，你可能会注意到更多可能发生的情况。以下是更多示例：

会议室预订系统
: 假设你想强制同一会议室在同一时间不能有两个预订[^55]。当有人想要预订时，你首先检查是否有任何冲突的预订（即，具有重叠时间范围的同一房间的预订），如果没有找到，你就创建会议（参见[例 8-2](#fig_transactions_meeting_rooms)）。
    
    {{< figure id="fig_transactions_meeting_rooms" title="例 8-2. 会议室预订系统试图避免重复预订（在快照隔离下不安全）" class="w-full my-4" >}}
    
    ```sql
    BEGIN TRANSACTION;
    
    -- 检查是否有任何现有预订与中午 12 点到 1 点的时间段重叠
    SELECT COUNT(*) FROM bookings
    WHERE room_id = 123 AND
    end_time > '2025-01-01 12:00' AND start_time < '2025-01-01 13:00';
    
    -- 如果前一个查询返回零：
    INSERT INTO bookings (room_id, start_time, end_time, user_id)
    VALUES (123, '2025-01-01 12:00', '2025-01-01 13:00', 666);
    
    COMMIT;
    ```

     不幸的是，快照隔离不会阻止另一个用户并发插入冲突的会议。为了保证你不会出现调度冲突，你再次需要可串行化隔离。

多人游戏
: 在[例 8-1](#fig_transactions_select_for_update) 中，我们使用锁来防止丢失的更新（即，确保两个玩家不能同时移动同一个棋子）。但是，锁不会阻止玩家将两个不同的棋子移动到棋盘上的同一位置，或者可能做出违反游戏规则的其他移动。根据你要执行的规则类型，你可能能够使用唯一约束，但否则你很容易受到写偏差的影响。

声明用户名
: 在每个用户都有唯一用户名的网站上，两个用户可能同时尝试使用相同的用户名创建账户。你可以使用事务来检查名称是否被占用，如果没有，使用该名称创建账户。但是，就像前面的例子一样，这在快照隔离下是不安全的。幸运的是，唯一约束在这里是一个简单的解决方案（尝试注册用户名的第二个事务将由于违反约束而被中止）。

防止重复消费
: 允许用户花钱或积分的服务需要检查用户不会花费超过他们拥有的。你可以通过在用户账户中插入暂定支出项目，列出账户中的所有项目，并检查总和是否为正来实现这一点。有了写偏差，可能会发生两个支出项目并发插入，它们一起导致余额变为负数，但没有任何事务注意到另一个。

#### 导致写偏差的幻读 {#sec_transactions_phantom}

所有这些例子都遵循类似的模式：

1. `SELECT` 查询通过搜索匹配某些搜索条件的行来检查是否满足某些要求（至少有两个医生值班，该房间在该时间没有现有预订，棋盘上的位置还没有另一个棋子，用户名尚未被占用，账户中仍有钱）。
2. 根据第一个查询的结果，应用程序代码决定如何继续（也许继续操作，或者向用户报告错误并中止）。
3. 如果应用程序决定继续，它会向数据库进行写入（`INSERT`、`UPDATE` 或 `DELETE`）并提交事务。

 此写入的效果改变了步骤 2 决策的前提条件。换句话说，如果你在提交写入后重复步骤 1 的 `SELECT` 查询，你会得到不同的结果，因为写入改变了匹配搜索条件的行集（现在少了一个医生值班，会议室现在已为该时间预订，棋盘上的位置现在被移动的棋子占据，用户名现在被占用，账户中的钱现在更少）。

步骤可能以不同的顺序发生。例如，你可以先进行写入，然后进行 `SELECT` 查询，最后根据查询结果决定是中止还是提交。

在医生值班示例的情况下，步骤 3 中被修改的行是步骤 1 中返回的行之一，因此我们可以通过锁定步骤 1 中的行（`SELECT FOR UPDATE`）来使事务安全并避免写偏差。但是，其他四个示例是不同的：它们检查*不存在*匹配某些搜索条件的行，而写入*添加*了匹配相同条件的行。如果步骤 1 中的查询不返回任何行，`SELECT FOR UPDATE` 就无法附加锁[^56]。

这种效果，其中一个事务中的写入改变另一个事务中搜索查询的结果，称为*幻读*[^4]。快照隔离避免了只读查询中的幻读，但在我们讨论的读写事务中，幻读可能导致特别棘手的写偏差情况。ORM 生成的 SQL 也容易出现写偏差[^50] [^51]。

#### 物化冲突 {#materializing-conflicts}

如果幻读的问题是没有对象可以附加锁，也许我们可以在数据库中人为地引入一个锁对象？

例如，在会议室预订情况下，你可以想象创建一个时间段和房间的表。此表中的每一行对应于特定时间段（例如，15 分钟）的特定房间。你提前为所有可能的房间和时间段组合创建行，例如，接下来的六个月。

现在，想要创建预订的事务可以锁定（`SELECT FOR UPDATE`）表中对应于所需房间和时间段的行。获取锁后，它可以像以前一样检查重叠的预订并插入新的预订。请注意，附加表不用于存储有关预订的信息——它纯粹是一组锁，用于防止同一房间和时间范围的预订被并发修改。

这种方法称为*物化冲突*，因为它采用了幻读并将其转化为存在于数据库中的具体行集上的锁冲突[^14]。不幸的是，很难且容易出错地弄清楚如何物化冲突，并且让并发控制机制泄漏到应用程序数据模型中是丑陋的。出于这些原因，如果没有其他选择，物化冲突应被视为最后的手段。在大多数情况下，可串行化隔离级别要好得多。



## 可串行化 {#sec_transactions_serializability}

在本章中，我们已经看到了几个容易出现竞态条件的事务示例。某些竞态条件被读已提交和快照隔离级别所防止，但其他的则没有。我们遇到了一些特别棘手的写偏差和幻读示例。这是一个令人沮丧的情况：

* 隔离级别很难理解，并且在不同数据库中的实现不一致（例如，"可重复读"的含义差异很大）。
* 如果你查看你的应用程序代码，很难判断在特定隔离级别下运行是否安全——特别是在大型应用程序中，你可能不知道所有可能并发发生的事情。
* 没有好的工具来帮助我们检测竞态条件。原则上，静态分析可能有所帮助[^33]，但研究技术尚未进入实际使用。测试并发问题很困难，因为它们通常是非确定性的——只有在时机不巧时才会出现问题。

这不是一个新问题——自 1970 年代引入弱隔离级别以来一直如此[^3]。一直以来，研究人员的答案都很简单：使用*可串行化*隔离！

可串行化隔离是最强的隔离级别。它保证即使事务可能并行执行，最终结果与它们*串行*执行（一次一个，没有任何并发）相同。因此，数据库保证如果事务在单独运行时行为正确，那么在并发运行时它们继续保持正确——换句话说，数据库防止了*所有*可能的竞态条件。

但如果可串行化隔离比弱隔离级别的混乱要好得多，那为什么不是每个人都在使用它？要回答这个问题，我们需要查看实现可串行化的选项，以及它们的性能如何。今天提供可串行化的大多数数据库使用以下三种技术之一，我们将在本章的其余部分探讨：

* 字面上串行执行事务（参见["实际串行执行"](#sec_transactions_serial)）
* 两阶段锁定（参见["两阶段锁定（2PL）"](#sec_transactions_2pl)），几十年来这是唯一可行的选择
* 乐观并发控制技术，如可串行化快照隔离（参见["可串行化快照隔离（SSI）"](#sec_transactions_ssi)）

### 实际串行执行 {#sec_transactions_serial}

避免并发问题的最简单方法是完全消除并发：在单个线程上按串行顺序一次执行一个事务。通过这样做，我们完全回避了检测和防止事务之间冲突的问题：所产生的隔离根据定义是可串行化的。

尽管这似乎是一个显而易见的想法，但直到 2000 年代，数据库设计者才决定执行事务的单线程循环是可行的[^57]。如果在过去 30 年中多线程并发被认为是获得良好性能的必要条件，那是什么改变使得单线程执行成为可能？

两个发展导致了这种重新思考：

* RAM 变得足够便宜，对于许多用例，现在可以将整个活动数据集保存在内存中（参见["将所有内容保存在内存中"](/ch4#sec_storage_inmemory)）。当事务需要访问的所有数据都在内存中时，事务的执行速度比必须等待从磁盘加载数据要快得多。
* 数据库设计者意识到 OLTP 事务通常很短，只进行少量读写（参见["分析与运营系统"](/ch1#sec_introduction_analytics)）。相比之下，长时间运行的分析查询通常是只读的，因此它们可以在串行执行循环之外的一致快照上运行（使用快照隔离）。

串行执行事务的方法在 VoltDB/H-Store、Redis 和 Datomic 等中实现[^58] [^59] [^60]。为单线程执行设计的系统有时可以比支持并发的系统性能更好，因为它可以避免锁定的协调开销。但是，其吞吐量限于单个 CPU 核心。为了充分利用该单线程，事务需要以不同于传统形式的方式构建。

#### 将事务封装在存储过程中 {#encapsulating-transactions-in-stored-procedures}

在数据库的早期，意图是数据库事务可以包含整个用户活动流程。例如，预订机票是一个多阶段过程（搜索路线、票价和可用座位；决定行程；预订行程中每个航班的座位；输入乘客详细信息；付款）。数据库设计者认为，如果整个过程是一个事务，以便可以原子地提交，那将是很好的。

不幸的是，人类做决定和响应的速度非常慢。如果数据库事务需要等待用户的输入，数据库需要支持潜在的大量并发事务，其中大多数是空闲的。大多数数据库无法有效地做到这一点，因此几乎所有 OLTP 应用程序都通过避免在事务中交互式地等待用户来保持事务简短。在 Web 上，这意味着事务在同一 HTTP 请求中提交——事务不跨越多个请求。新的 HTTP 请求开始新的事务。

即使人类已经从关键路径中移除，事务仍然以交互式客户端/服务器风格执行，一次一个语句。应用程序进行查询，读取结果，可能根据第一个查询的结果进行另一个查询，依此类推。查询和结果在应用程序代码（在一台机器上运行）和数据库服务器（在另一台机器上）之间来回发送。

在这种交互式事务风格中，大量时间花在应用程序和数据库之间的网络通信上。如果你要在数据库中禁止并发并一次只处理一个事务，吞吐量将是可怕的，因为数据库将大部分时间都在等待应用程序为当前事务发出下一个查询。在这种数据库中，为了获得合理的性能，必须并发处理多个事务。

因此，具有单线程串行事务处理的系统不允许交互式多语句事务。相反，应用程序必须将自己限制为包含单个语句的事务，或者提前将整个事务代码作为*存储过程*提交给数据库[^61]。

交互式事务和存储过程之间的差异如[图 8-9](#fig_transactions_stored_proc) 所示。前提是事务所需的所有数据都在内存中，存储过程可以非常快速地执行，而无需等待任何网络或磁盘 I/O。

{{< figure src="/fig/ddia_0809.png" id="fig_transactions_stored_proc" caption="图 8-9. 交互式事务和存储过程之间的差异（使用[图 8-8](#fig_transactions_write_skew)的示例事务）。" class="w-full my-4" >}}

#### 存储过程的利弊 {#sec_transactions_stored_proc_tradeoffs}

存储过程在关系数据库中已经存在了一段时间，自 1999 年以来一直是 SQL 标准（SQL/PSM）的一部分。它们因各种原因获得了一些不好的声誉：

* 传统上，每个数据库供应商都有自己的存储过程语言（Oracle 有 PL/SQL，SQL Server 有 T-SQL，PostgreSQL 有 PL/pgSQL 等）。这些语言没有跟上通用编程语言的发展，因此从今天的角度来看，它们看起来相当丑陋和过时，并且缺乏大多数编程语言中的库生态系统。
* 在数据库中运行的代码很难管理：与应用程序服务器相比，调试更困难，版本控制和部署更尴尬，测试更棘手，并且难以与监控的指标收集系统集成。
* 数据库通常比应用程序服务器对性能更敏感，因为单个数据库实例通常由许多应用程序服务器共享。数据库中编写不当的存储过程（例如，使用大量内存或 CPU 时间）可能比应用程序服务器中等效的编写不当的代码造成更多麻烦。
* 在允许租户编写自己的存储过程的多租户系统中，在与数据库内核相同的进程中执行不受信任的代码是一个安全风险[^62]。

然而，这些问题可以克服。存储过程的现代实现已经放弃了 PL/SQL，而是使用现有的通用编程语言：VoltDB 使用 Java 或 Groovy，Datomic 使用 Java 或 Clojure，Redis 使用 Lua，MongoDB 使用 Javascript。

存储过程在应用程序逻辑无法轻松嵌入其他地方的情况下也很有用。例如，使用 GraphQL 的应用程序可能通过 GraphQL 代理直接公开其数据库。如果代理不支持复杂的验证逻辑，你可以使用存储过程将此类逻辑直接嵌入数据库中。如果数据库不支持存储过程，你必须在代理和数据库之间部署验证服务来进行验证。

使用存储过程和内存数据，在单个线程上执行所有事务变得可行。当存储过程不需要等待 I/O 并避免其他并发控制机制的开销时，它们可以在单个线程上实现相当好的吞吐量。

VoltDB 还使用存储过程进行复制：它不是将事务的写入从一个节点复制到另一个节点，而是在每个副本上执行相同的存储过程。因此，VoltDB 要求存储过程是*确定性的*（在不同节点上运行时，它们必须产生相同的结果）。例如，如果事务需要使用当前日期和时间，它必须通过特殊的确定性 API 来实现（有关确定性操作的更多详细信息，请参见["持久执行和工作流"](/ch5#sec_encoding_dataflow_workflows)）。这种方法称为*状态机复制*，我们将在[第 10 章](/ch10#ch_consistency)中回到它。

#### 分片 {#sharding}

串行执行所有事务使并发控制变得简单得多，但将数据库的事务吞吐量限制为单台机器上单个 CPU 核心的速度。只读事务可以使用快照隔离在其他地方执行，但对于具有高写入吞吐量的应用程序，单线程事务处理器可能成为严重的瓶颈。

为了扩展到多个 CPU 核心和多个节点，你可以对数据进行分片（参见[第 7 章](/ch7#ch_sharding)），VoltDB 支持这一点。如果你可以找到一种对数据集进行分片的方法，使每个事务只需要读取和写入单个分片内的数据，那么每个分片可以有自己的事务处理线程，独立于其他分片运行。在这种情况下，你可以给每个 CPU 核心分配自己的分片，这允许你的事务吞吐量与 CPU 核心数量线性扩展[^59]。

但是，对于需要访问多个分片的任何事务，数据库必须协调它所涉及的所有分片之间的事务。存储过程需要在所有分片上同步执行，以确保整个系统的可串行化。

由于跨分片事务具有额外的协调开销，因此它们比单分片事务慢得多。VoltDB 报告的跨分片写入吞吐量约为每秒 1,000 次，这比其单分片吞吐量低几个数量级，并且无法通过添加更多机器来增加[^61]。最近的研究探索了使多分片事务更具可伸缩性的方法[^63]。

事务是否可以是单分片的很大程度上取决于应用程序使用的数据结构。简单的键值数据通常可以很容易地分片，但具有多个二级索引的数据可能需要大量的跨分片协调（参见["分片和二级索引"](/ch7#sec_sharding_secondary_indexes)）。

#### 串行执行总结 {#summary-of-serial-execution}

串行执行事务已成为在某些约束条件下实现可串行化隔离的可行方法：

* 每个事务必须小而快，因为只需要一个缓慢的事务就可以阻止所有事务处理。
* 它最适合活动数据集可以适合内存的情况。很少访问的数据可能会移到磁盘，但如果需要在单线程事务中访问，系统会变得非常慢。
* 写入吞吐量必须足够低，可以在单个 CPU 核心上处理，否则事务需要分片而不需要跨分片协调。
* 跨分片事务是可能的，但它们的吞吐量很难扩展。

### 两阶段锁定（2PL） {#sec_transactions_2pl}

大约 30 年来，数据库中只有一种广泛使用的可串行化算法：*两阶段锁定*（2PL），有时称为*强严格两阶段锁定*（SS2PL），以区别于 2PL 的其他变体。


--------

> [!TIP] 2PL 不是 2PC

两阶段*锁定*（2PL）和两阶段*提交*（2PC）是两个非常不同的东西。2PL 提供可串行化隔离，而 2PC 在分布式数据库中提供原子提交（参见["两阶段提交（2PC）"](#sec_transactions_2pc)）。为避免混淆，最好将它们视为完全独立的概念，并忽略名称中不幸的相似性。

--------

我们之前看到锁通常用于防止脏写（参见["没有脏写"](#sec_transactions_dirty_write)）：如果两个事务并发尝试写入同一对象，锁确保第二个写入者必须等到第一个完成其事务（中止或提交）后才能继续。

两阶段锁定类似，但使锁要求更强。只要没有人写入，多个事务就可以并发读取同一对象。但是一旦有人想要写入（修改或删除）对象，就需要独占访问：

* 如果事务 A 已读取对象而事务 B 想要写入该对象，B 必须等到 A 提交或中止后才能继续。（这确保 B 不能在 A 背后意外地更改对象。）
* 如果事务 A 已写入对象而事务 B 想要读取该对象，B 必须等到 A 提交或中止后才能继续。（像[图 8-4](#fig_transactions_read_committed) 中那样读取对象的旧版本在 2PL 下是不可接受的。）

在 2PL 中，写入者不仅阻塞其他写入者；它们还阻塞读者，反之亦然。快照隔离有这样的口号：*读者永远不会阻塞写者，写者永远不会阻塞读者*（参见["多版本并发控制（MVCC）"](#sec_transactions_snapshot_impl)），这捕捉了快照隔离和两阶段锁定之间的关键区别。另一方面，因为 2PL 提供可串行化，它可以防止早期讨论的所有竞态条件，包括丢失的更新和写偏差。

#### 两阶段锁定的实现 {#implementation-of-two-phase-locking}

2PL 由 MySQL（InnoDB）和 SQL Server 中的可串行化隔离级别以及 Db2 中的可重复读隔离级别使用[^29]。

读者和写者的阻塞是通过在数据库中的每个对象上有一个锁来实现的。锁可以处于*共享模式*或*独占模式*（也称为*多读者单写者*锁）。锁的使用如下：

* 如果事务想要读取对象，它必须首先以共享模式获取锁。多个事务可以同时以共享模式持有锁，但如果另一个事务已经对该对象具有独占锁，则这些事务必须等待。
* 如果事务想要写入对象，它必须首先以独占模式获取锁。没有其他事务可以同时持有锁（无论是共享模式还是独占模式），因此如果对象上有任何现有锁，事务必须等待。
* 如果事务首先读取然后写入对象，它可以将其共享锁升级为独占锁。升级的工作方式与直接获取独占锁相同。
* 获取锁后，事务必须继续持有锁直到事务结束（提交或中止）。这就是"两阶段"名称的来源：第一阶段（事务执行时）是获取锁，第二阶段（事务结束时）是释放所有锁。

由于使用了如此多的锁，很容易发生事务 A 等待事务 B 释放其锁，反之亦然的情况。这种情况称为*死锁*。数据库自动检测事务之间的死锁并中止其中一个，以便其他事务可以取得进展。中止的事务需要由应用程序重试。

#### 两阶段锁定的性能 {#performance-of-two-phase-locking}

两阶段锁定的主要缺点，以及自 1970 年代以来并非每个人都使用它的原因，是性能：在两阶段锁定下，事务吞吐量和查询响应时间明显比弱隔离下差。

这部分是由于获取和释放所有这些锁的开销，但更重要的是由于并发性降低。按设计，如果两个并发事务尝试执行任何可能以任何方式导致竞态条件的操作，其中一个必须等待另一个完成。

例如，如果你有一个需要读取整个表的事务（例如，备份、分析查询或完整性检查，如["快照隔离与可重复读"](#sec_transactions_snapshot_isolation)中所讨论的），该事务必须对整个表进行共享锁。因此，读取事务首先必须等到所有正在写入该表的进行中事务完成；然后，在读取整个表时（对于大表可能需要很长时间），所有想要写入该表的其他事务都被阻塞，直到大型只读事务提交。实际上，数据库在很长一段时间内无法进行写入。

因此，运行 2PL 的数据库可能具有相当不稳定的延迟，如果工作负载中存在争用，它们在高百分位数可能非常慢（参见["描述性能"](/ch2#sec_introduction_percentiles)）。可能只需要一个缓慢的事务，或者一个访问大量数据并获取许多锁的事务，就会导致系统的其余部分停滞不前。

尽管死锁可能发生在基于锁的读已提交隔离级别下，但在 2PL 可串行化隔离下（取决于事务的访问模式）它们发生得更频繁。这可能是一个额外的性能问题：当事务由于死锁而被中止并重试时，它需要重新完成所有工作。如果死锁频繁，这可能意味着大量的浪费努力。

#### 谓词锁 {#predicate-locks}

在前面的锁描述中，我们掩盖了一个微妙但重要的细节。在["导致写偏差的幻读"](#sec_transactions_phantom)中，我们讨论了*幻读*的问题——即一个事务改变另一个事务的搜索查询结果。具有可串行化隔离的数据库必须防止幻读。

在会议室预订示例中，这意味着如果一个事务已经搜索了某个时间窗口内某个房间的现有预订（参见[例 8-2](#fig_transactions_meeting_rooms)），另一个事务不允许并发插入或更新同一房间和时间范围的另一个预订。（并发插入其他房间的预订，或同一房间不影响拟议预订的不同时间的预订是可以的。）

我们如何实现这一点？从概念上讲，我们需要一个*谓词锁*[^4]。它的工作方式类似于前面描述的共享/独占锁，但它不属于特定对象（例如，表中的一行），而是属于匹配某些搜索条件的所有对象，例如：

```
SELECT * FROM bookings
 WHERE room_id = 123 AND
 end_time > '2025-01-01 12:00' AND
 start_time < '2025-01-01 13:00';
```

谓词锁限制访问如下：

* 如果事务 A 想要读取匹配某些条件的对象，就像在该 `SELECT` 查询中一样，它必须在查询条件上获取共享模式谓词锁。如果另一个事务 B 当前对匹配这些条件的任何对象具有独占锁，A 必须等到 B 释放其锁后才允许进行查询。
* 如果事务 A 想要插入、更新或删除任何对象，它必须首先检查旧值或新值是否匹配任何现有的谓词锁。如果存在事务 B 持有的匹配谓词锁，则 A 必须等到 B 提交或中止后才能继续。

这里的关键思想是，谓词锁甚至适用于数据库中尚不存在但将来可能添加的对象（幻读）。如果两阶段锁定包括谓词锁，数据库将防止所有形式的写偏差和其他竞态条件，因此其隔离变为可串行化。

#### 索引范围锁 {#sec_transactions_2pl_range}

不幸的是，谓词锁的性能不佳：如果活动事务有许多锁，检查匹配锁变得耗时。因此，大多数具有 2PL 的数据库实际上实现了*索引范围锁定*（也称为*间隙锁*），这是谓词锁定的简化近似[^54] [^64]。

通过使谓词匹配更大的对象集来简化谓词是安全的。例如，如果你对中午到下午 1 点之间房间 123 的预订有谓词锁，你可以通过锁定房间 123 在任何时间的预订来近似它，或者你可以通过锁定中午到下午 1 点之间的所有房间（不仅仅是房间 123）来近似它。这是安全的，因为匹配原始谓词的任何写入肯定也会匹配近似。

在房间预订数据库中，你可能在 `room_id` 列上有索引，和/或在 `start_time` 和 `end_time` 上有索引（否则前面的查询在大型数据库上会非常慢）：

* 假设你的索引在 `room_id` 上，数据库使用此索引查找房间 123 的现有预订。现在数据库可以简单地将共享锁附加到此索引条目，表示事务已搜索房间 123 的预订。
* 或者，如果数据库使用基于时间的索引查找现有预订，它可以将共享锁附加到该索引中的值范围，表示事务已搜索与 2025 年 1 月 1 日中午到下午 1 点的时间段重叠的预订。

无论哪种方式，搜索条件的近似都附加到其中一个索引。现在，如果另一个事务想要插入、更新或删除同一房间和/或重叠时间段的预订，它将必须更新索引的相同部分。在这样做的过程中，它将遇到共享锁，并被迫等到锁被释放。

这提供了对幻读和写偏差的有效保护。索引范围锁不如谓词锁精确（它们可能锁定比严格维护可串行化所需的更大范围的对象），但由于它们的开销要低得多，它们是一个很好的折衷。

如果没有合适的索引可以附加范围锁，数据库可以退回到整个表的共享锁。这对性能不利，因为它将阻止所有其他事务写入表，但这是一个安全的后备位置。

### 可串行化快照隔离（SSI） {#sec_transactions_ssi}

本章描绘了数据库并发控制的黯淡画面。一方面，我们有性能不佳（两阶段锁定）或可伸缩性不佳（串行执行）的可串行化实现。另一方面，我们有性能良好但容易出现各种竞态条件（丢失的更新、写偏差、幻读等）的弱隔离级别。可串行化隔离和良好性能从根本上是对立的吗？

似乎不是：一种称为*可串行化快照隔离*（SSI）的算法提供完全可串行化，与快照隔离相比只有很小的性能损失。SSI 相对较新：它于 2008 年首次描述[^53] [^65]。

今天，SSI 和类似算法用于单节点数据库（PostgreSQL 中的可串行化隔离级别[^54]、SQL Server 的内存 OLTP/Hekaton[^66] 和 HyPer[^67]）、分布式数据库（CockroachDB[^5] 和 FoundationDB[^8]）以及嵌入式存储引擎（如 BadgerDB）。

#### 悲观并发控制与乐观并发控制 {#pessimistic-versus-optimistic-concurrency-control}

两阶段锁定是所谓的*悲观*并发控制机制：它基于这样的原则，即如果任何事情可能出错（如另一个事务持有的锁所示），最好等到情况再次安全后再做任何事情。它就像*互斥*，用于保护多线程编程中的数据结构。

串行执行在某种意义上是悲观到极端：它本质上相当于每个事务在事务期间对整个数据库（或数据库的一个分片）具有独占锁。我们通过使每个事务执行得非常快来补偿悲观主义，因此它只需要短时间持有"锁"。

相比之下，可串行化快照隔离是一种*乐观*并发控制技术。在这种情况下，乐观意味着，如果发生潜在危险的事情，事务不会阻塞，而是继续进行，希望一切都会好起来。当事务想要提交时，数据库会检查是否发生了任何不好的事情（即，是否违反了隔离）；如果是，事务将被中止并必须重试。只允许可串行执行的事务提交。

乐观并发控制是一个老想法[^68]，其优缺点已经争论了很长时间[^69]。如果存在高争用（许多事务尝试访问相同的对象），它的性能很差，因为这会导致大部分事务需要中止。如果系统已经接近其最大吞吐量，重试事务的额外事务负载可能会使性能变差。

但是，如果有足够的备用容量，并且事务之间的争用不太高，乐观并发控制技术往往比悲观技术性能更好。可交换原子操作可以减少争用：例如，如果几个事务并发想要递增计数器，应用递增的顺序无关紧要（只要计数器在同一事务中没有被读取），因此并发递增都可以应用而不会发生冲突。

顾名思义，SSI 基于快照隔离——也就是说，事务中的所有读取都从数据库的一致快照进行（参见["快照隔离与可重复读"](#sec_transactions_snapshot_isolation)）。在快照隔离的基础上，SSI 添加了一种算法来检测读写之间的串行化冲突，并确定要中止哪些事务。

#### 基于过时前提的决策 {#decisions-based-on-an-outdated-premise}

当我们之前讨论快照隔离中的写偏差时（参见["写偏差与幻读"](#sec_transactions_write_skew)），我们观察到一个反复出现的模式：事务从数据库读取一些数据，检查查询结果，并根据它看到的结果决定采取某些行动（写入数据库）。但是，在快照隔离下，原始查询的结果在事务提交时可能不再是最新的，因为数据可能在此期间被修改。

换句话说，事务基于*前提*（事务开始时为真的事实，例如，"当前有两名医生值班"）采取行动。后来，当事务想要提交时，原始数据可能已更改——前提可能不再为真。

当应用程序进行查询（例如，"当前有多少医生值班？"）时，数据库不知道应用程序逻辑如何使用该查询的结果。为了安全起见，数据库需要假设查询结果（前提）中的任何更改都意味着该事务中的写入可能无效。换句话说，事务中的查询和写入之间可能存在因果依赖关系。为了提供可串行化隔离，数据库必须检测事务可能基于过时前提采取行动的情况，并在这种情况下中止事务。

数据库如何知道查询结果是否可能已更改？有两种情况需要考虑：

* 检测陈旧的 MVCC 对象版本的读取（未提交的写入发生在读取之前）
* 检测影响先前读取的写入（写入发生在读取之后）

#### 检测陈旧的 MVCC 读取 {#detecting-stale-mvcc-reads}

回想一下，快照隔离通常由多版本并发控制（MVCC；参见["多版本并发控制（MVCC）"](#sec_transactions_snapshot_impl)）实现。当事务从 MVCC 数据库中的一致快照读取时，它会忽略在拍摄快照时尚未提交的任何其他事务所做的写入。

在[图 8-10](#fig_transactions_detect_mvcc) 中，事务 43 看到 Aaliyah 的 `on_call = true`，因为事务 42（修改了 Aaliyah 的值班状态）未提交。但是，当事务 43 想要提交时，事务 42 已经提交。这意味着从一致快照读取时被忽略的写入现在已生效，事务 43 的前提不再为真。当写入者插入以前不存在的数据时，事情变得更加复杂（参见["导致写偏差的幻读"](#sec_transactions_phantom)）。我们将在["检测影响先前读取的写入"](#sec_detecting_writes_affect_reads)中讨论为 SSI 检测幻写。

{{< figure src="/fig/ddia_0810.png" id="fig_transactions_detect_mvcc" caption="图 8-10. 检测事务何时从 MVCC 快照读取过时值。" class="w-full my-4" >}}


为了防止这种异常，数据库需要跟踪事务由于 MVCC 可见性规则而忽略另一个事务的写入的时间。当事务想要提交时，数据库会检查是否有任何被忽略的写入现在已经提交。如果是，事务必须被中止。

为什么要等到提交？为什么不在检测到陈旧读取时立即中止事务 43？好吧，如果事务 43 是只读事务，它就不需要被中止，因为没有写偏差的风险。在事务 43 进行读取时，数据库还不知道该事务是否稍后会执行写入。此外，事务 42 可能还会中止，或者在事务 43 提交时可能仍未提交，因此读取可能最终不是陈旧的。通过避免不必要的中止，SSI 保留了快照隔离对从一致快照进行长时间运行读取的支持。

#### 检测影响先前读取的写入 {#sec_detecting_writes_affect_reads}

要考虑的第二种情况是另一个事务在数据被读取后修改数据。这种情况如[图 8-11](#fig_transactions_detect_index_range) 所示。

{{< figure src="/fig/ddia_0811.png" id="fig_transactions_detect_index_range" caption="图 8-11. 在可串行化快照隔离中，检测一个事务何时修改另一个事务的读取。" class="w-full my-4" >}}


在两阶段锁定的上下文中，我们讨论了索引范围锁（参见["索引范围锁"](#sec_transactions_2pl_range)），它允许数据库锁定对匹配某些搜索查询的所有行的访问，例如 `WHERE shift_id = 1234`。我们可以在这里使用类似的技术，除了 SSI 锁不会阻塞其他事务。

在[图 8-11](#fig_transactions_detect_index_range) 中，事务 42 和 43 都在班次 `1234` 期间搜索值班医生。如果 `shift_id` 上有索引，数据库可以使用索引条目 1234 来记录事务 42 和 43 读取此数据的事实。（如果没有索引，可以在表级别跟踪此信息。）此信息只需要保留一段时间：在事务完成（提交或中止）并且所有并发事务完成后，数据库可以忘记它读取的数据。

当事务写入数据库时，它必须在索引中查找最近读取受影响数据的任何其他事务。此过程类似于获取受影响键范围的写锁，但它不是阻塞直到读者提交，而是充当绊线：它只是通知事务它们读取的数据可能不再是最新的。

在[图 8-11](#fig_transactions_detect_index_range) 中，事务 43 通知事务 42 其先前的读取已过时，反之亦然。事务 42 首先提交，并且成功：尽管事务 43 的写入影响了 42，但 43 尚未提交，因此写入尚未生效。但是，当事务 43 想要提交时，来自 42 的冲突写入已经提交，因此 43 必须中止。

#### 可串行化快照隔离的性能 {#performance-of-serializable-snapshot-isolation}

与往常一样，许多工程细节会影响算法在实践中的工作效果。例如，一个权衡是跟踪事务读写的粒度。如果数据库详细跟踪每个事务的活动，它可以精确地确定哪些事务需要中止，但簿记开销可能变得很大。不太详细的跟踪速度更快，但可能导致比严格必要更多的事务被中止。

在某些情况下，事务读取被另一个事务覆盖的信息是可以的：根据发生的其他情况，有时可以证明执行结果仍然是可串行化的。PostgreSQL 使用这一理论来减少不必要中止的数量[^14] [^54]。

与两阶段锁定相比，可串行化快照隔离的主要优点是一个事务不需要阻塞等待另一个事务持有的锁。与快照隔离一样，写入者不会阻塞读者，反之亦然。这种设计原则使查询延迟更可预测且变化更少。特别是，只读查询可以在一致快照上运行而无需任何锁，这对于读取密集型工作负载非常有吸引力。

与串行执行相比，可串行化快照隔离不限于单个 CPU 核心的吞吐量：例如，FoundationDB 将串行化冲突的检测分布在多台机器上，允许它扩展到非常高的吞吐量。即使数据可能分片在多台机器上，事务也可以在多个分片中读取和写入数据，同时确保可串行化隔离。

与非可串行化快照隔离相比，检查可串行化违规的需要引入了一些性能开销。这些开销有多大是一个争论的问题：有些人认为可串行化检查不值得[^70]，而其他人认为可串行化的性能现在已经很好，不再需要使用较弱的快照隔离[^67]。

中止率显著影响 SSI 的整体性能。例如，长时间读取和写入数据的事务可能会遇到冲突并中止，因此 SSI 要求读写事务相当短（长时间运行的只读事务是可以的）。但是，SSI 对慢事务的敏感性低于两阶段锁定或串行执行。

## 分布式事务 {#sec_transactions_distributed}

前几节重点讨论了隔离的并发控制，即 ACID 中的 I。我们看到的算法适用于单节点和分布式数据库：尽管在使并发控制算法可扩展方面存在挑战（例如，为 SSI 执行分布式可串行化检查），但分布式并发控制的高层思想与单节点并发控制相似[^8]。

一致性和持久性在转向分布式事务时也没有太大变化。但是，原子性需要更多关注。

对于在单个数据库节点执行的事务，原子性通常由存储引擎实现。当客户端要求数据库节点提交事务时，数据库使事务的写入持久化（通常在预写日志中；参见["使 B 树可靠"](/ch4#sec_storage_btree_wal)），然后将提交记录附加到磁盘上的日志。如果数据库在此过程中崩溃，事务将在节点重新启动时从日志中恢复：如果提交记录在崩溃前成功写入磁盘，则事务被认为已提交；如果没有，该事务的任何写入都将回滚。

因此，在单个节点上，事务提交关键取决于数据持久写入磁盘的*顺序*：首先是数据，然后是提交记录[^22]。事务提交或中止的关键决定时刻是磁盘完成写入提交记录的时刻：在那一刻之前，仍然可能中止（由于崩溃），但在那一刻之后，事务已提交（即使数据库崩溃）。因此，是单个设备（连接到特定节点的特定磁盘驱动器的控制器）使提交成为原子的。

但是，如果多个节点参与事务会怎样？例如，也许你在分片数据库中有多对象事务，或者有全局二级索引（其中索引条目可能与主数据在不同的节点上；参见["分片和二级索引"](/ch7#sec_sharding_secondary_indexes)）。大多数"NoSQL"分布式数据存储不支持此类分布式事务，但各种分布式关系数据库支持。

在这些情况下，仅向所有节点发送提交请求并在每个节点上独立提交事务是不够的。如[图 8-12](#fig_transactions_non_atomic) 所示，提交可能在某些节点上成功，在其他节点上失败：

* 某些节点可能检测到约束违规或冲突，需要中止，而其他节点能够成功提交。
* 某些提交请求可能在网络中丢失，最终由于超时而中止，而其他提交请求通过。
* 某些节点可能在提交记录完全写入之前崩溃并在恢复时回滚，而其他节点成功提交。

{{< figure src="/fig/ddia_0812.png" id="fig_transactions_non_atomic" caption="图 8-12. 当事务涉及多个数据库节点时，它可能在某些节点上提交，在其他节点上失败。" class="w-full my-4" >}}


如果某些节点提交事务而其他节点中止它，节点之间就会变得不一致。一旦事务在一个节点上提交，如果后来发现它在另一个节点上被中止，就不能撤回了。这是因为一旦数据被提交，它在*读已提交*或更强的隔离下对其他事务可见。例如，在[图 8-12](#fig_transactions_non_atomic) 中，当用户 1 注意到其在数据库 1 上的提交失败时，用户 2 已经从数据库 2 上的同一事务读取了数据。如果用户 1 的事务后来被中止，用户 2 的事务也必须被还原，因为它基于被追溯声明不存在的数据。

更好的方法是确保参与事务的节点要么全部提交，要么全部中止，并防止两者的混合。确保这一点被称为*原子提交*问题。

### 两阶段提交（2PC） {#sec_transactions_2pc}

两阶段提交是一种跨多个节点实现原子事务提交的算法。它是分布式数据库中的经典算法[^13] [^71] [^72]。2PC 在某些数据库内部使用，也以 *XA 事务*[^73] 的形式提供给应用程序（例如，Java 事务 API 支持），或通过 WS-AtomicTransaction 用于 SOAP Web 服务[^74] [^75]。

2PC 的基本流程如[图 8-13](#fig_transactions_two_phase_commit) 所示。与单节点事务的单个提交请求不同，2PC 中的提交/中止过程分为两个阶段（因此得名）。

{{< figure src="/fig/ddia_0813.png" id="fig_transactions_two_phase_commit" title="图 8-13. 两阶段提交（2PC）的成功执行。" class="w-full my-4" >}}


2PC 使用一个通常不会出现在单节点事务中的新组件：*协调器*（也称为*事务管理器*）。协调器通常作为请求事务的同一应用程序进程中的库实现（例如，嵌入在 Java EE 容器中），但它也可以是单独的进程或服务。此类协调器的示例包括 Narayana、JOTM、BTM 或 MSDTC。

使用 2PC 时，分布式事务从应用程序在多个数据库节点上正常读写数据开始。我们称这些数据库节点为事务中的*参与者*。当应用程序准备提交时，协调器开始第 1 阶段：它向每个节点发送*准备*请求，询问它们是否能够提交。然后协调器跟踪参与者的响应：

* 如果所有参与者回复"是"，表示他们准备提交，那么协调器在第 2 阶段发出*提交*请求，提交实际发生。
* 如果任何参与者回复"否"，协调器在第 2 阶段向所有节点发送*中止*请求。

这个过程有点像西方文化中的传统婚礼仪式：牧师分别询问新娘和新郎是否愿意嫁给对方，通常从两人那里得到"我愿意"的答案。在收到两个确认后，牧师宣布这对夫妇为夫妻：事务已提交，这个快乐的事实向所有参加者广播。如果新娘或新郎没有说"是"，仪式就被中止了[^76]。

#### 系统性的承诺 {#a-system-of-promises}

从这个简短的描述中，可能不清楚为什么两阶段提交确保原子性，而跨多个节点的单阶段提交却不能。准备和提交请求在两阶段情况下同样容易丢失。是什么让 2PC 不同？

要理解它为什么有效，我们必须更详细地分解这个过程：

1. 当应用程序想要开始分布式事务时，它从协调器请求事务 ID。此事务 ID 是全局唯一的。
2. 应用程序在每个参与者上开始单节点事务，并将全局唯一的事务 ID 附加到单节点事务。所有读写都在这些单节点事务之一中完成。如果在此阶段出现任何问题（例如，节点崩溃或请求超时），协调器或任何参与者都可以中止。
3. 当应用程序准备提交时，协调器向所有参与者发送准备请求，标记有全局事务 ID。如果这些请求中的任何一个失败或超时，协调器向所有参与者发送该事务 ID 的中止请求。
4. 当参与者收到准备请求时，它确保它可以在任何情况下明确提交事务。

 这包括将所有事务数据写入磁盘（崩溃、电源故障或磁盘空间不足不是稍后拒绝提交的可接受借口），并检查任何冲突或约束违规。通过向协调器回复"是"，节点承诺在请求时无错误地提交事务。换句话说，参与者放弃了中止事务的权利，但没有实际提交它。
5. 当协调器收到所有准备请求的响应时，它对是否提交或中止事务做出明确决定（仅当所有参与者投票"是"时才提交）。协调器必须将该决定写入其磁盘上的事务日志，以便在随后崩溃时知道它是如何决定的。这称为*提交点*。
6. 一旦协调器的决定被写入磁盘，提交或中止请求就会发送给所有参与者。如果此请求失败或超时，协调器必须永远重试，直到成功。没有回头路：如果决定是提交，那么必须执行该决定，无论需要多少次重试。如果参与者在此期间崩溃，事务将在恢复时提交——因为参与者投票"是"，它在恢复时不能拒绝提交。

因此，该协议包含两个关键的"不归路"：当参与者投票"是"时，它承诺它肯定能够稍后提交（尽管协调器仍可能选择中止）；一旦协调器决定，该决定是不可撤销的。这些承诺确保了 2PC 的原子性。（单节点原子提交将这两个事件合并为一个：将提交记录写入事务日志。）

回到婚姻比喻，在说"我愿意"之前，你和你的新娘/新郎有自由通过说"不行！"（或类似的话）来中止事务。但是，在说"我愿意"之后，你不能撤回该声明。如果你在说"我愿意"后晕倒，没有听到牧师说"你们现在是夫妻"，这并不改变事务已提交的事实。当你稍后恢复意识时，你可以通过向牧师查询你的全局事务 ID 的状态来了解你是否已婚，或者你可以等待牧师下一次重试提交请求（因为重试将在你失去意识期间继续）。

#### 协调器故障 {#coordinator-failure}

我们已经讨论了如果参与者之一或网络在 2PC 期间失败会发生什么：如果任何准备请求失败或超时，协调器将中止事务；如果任何提交或中止请求失败，协调器将无限期地重试它们。但是，如果协调器崩溃会发生什么就不太清楚了。

如果协调器在发送准备请求之前失败，参与者可以安全地中止事务。但是一旦参与者收到准备请求并投票"是"，它就不能再单方面中止——它必须等待协调器回复事务是提交还是中止。如果协调器此时崩溃或网络失败，参与者除了等待别无他法。参与者在此状态下的事务称为*存疑*或*不确定*。

这种情况如[图 8-14](#fig_transactions_2pc_crash) 所示。在这个特定的例子中，协调器实际上决定提交，数据库 2 收到了提交请求。但是，协调器在向数据库 1 发送提交请求之前崩溃了，因此数据库 1 不知道是提交还是中止。即使超时在这里也没有帮助：如果数据库 1 在超时后单方面中止，它将与已提交的数据库 2 不一致。同样，单方面提交也不安全，因为另一个参与者可能已中止。

{{< figure src="/fig/ddia_0814.png" id="fig_transactions_2pc_crash" title="图 8-14. 协调器在参与者投票“是”后崩溃。数据库 1 不知道是提交还是中止。" class="w-full my-4" >}}


没有协调器的消息，参与者无法知道是提交还是中止。原则上，参与者可以相互通信，了解每个参与者如何投票并达成某种协议，但这不是 2PC 协议的一部分。

2PC 完成的唯一方法是等待协调器恢复。这就是为什么协调器必须在向参与者发送提交或中止请求之前将其提交或中止决定写入磁盘上的事务日志：当协调器恢复时，它通过读取其事务日志来确定所有存疑事务的状态。协调器日志中没有提交记录的任何事务都将中止。因此，2PC 的提交点归结为协调器上的常规单节点原子提交。

#### 三阶段提交 {#three-phase-commit}

由于 2PC 可能会卡住等待协调器恢复，因此两阶段提交被称为*阻塞*原子提交协议。可以使原子提交协议*非阻塞*，以便在节点失败时不会卡住。但是，在实践中使其工作并不那么简单。

作为 2PC 的替代方案，已经提出了一种称为*三阶段提交*（3PC）的算法[^13] [^77]。但是，3PC 假设具有有界延迟的网络和具有有界响应时间的节点；在大多数具有无界网络延迟和进程暂停的实际系统中（参见[第 9 章](/ch9#ch_distributed)），它无法保证原子性。

实践中更好的解决方案是用容错共识协议替换单节点协调器。我们将在[第 10 章](/ch10#ch_consistency)中看到如何做到这一点。

### 跨不同系统的分布式事务 {#sec_transactions_xa}

分布式事务和两阶段提交的声誉参差不齐。一方面，它们被认为提供了一个重要的安全保证，否则很难实现；另一方面，它们因导致操作问题、扼杀性能并承诺超过它们可以提供的东西而受到批评[^78] [^79] [^80] [^81]。许多云服务由于它们引起的操作问题而选择不实现分布式事务[^82]。

某些分布式事务的实现会带来沉重的性能损失。两阶段提交固有的大部分性能成本是由于崩溃恢复所需的额外磁盘强制（`fsync`）和额外的网络往返。

但是，与其直接否定分布式事务，我们应该更详细地研究它们，因为从中可以学到重要的教训。首先，我们应该准确说明"分布式事务"的含义。两种完全不同类型的分布式事务经常被混淆：

数据库内部分布式事务
: 某些分布式数据库（即，在其标准配置中使用复制和分片的数据库）支持该数据库节点之间的内部事务。例如，YugabyteDB、TiDB、FoundationDB、Spanner、VoltDB 和 MySQL Cluster 的 NDB 存储引擎都有这样的内部事务支持。在这种情况下，参与事务的所有节点都运行相同的数据库软件。

异构分布式事务
: 在*异构*事务中，参与者是两个或多个不同的技术：例如，来自不同供应商的两个数据库，甚至是非数据库系统（如消息代理）。跨这些系统的分布式事务必须确保原子提交，即使系统在底层可能完全不同。

数据库内部事务不必与任何其他系统兼容，因此它们可以使用任何协议并应用特定于该特定技术的优化。因此，数据库内部分布式事务通常可以很好地工作。另一方面，跨异构技术的事务更具挑战性。

#### 恰好一次消息处理 {#sec_transactions_exactly_once}

异构分布式事务允许以强大的方式集成各种系统。例如，当且仅当处理消息的数据库事务成功提交时，来自消息队列的消息才能被确认为已处理。这是通过在单个事务中原子地提交消息确认和数据库写入来实现的。有了分布式事务支持，即使消息代理和数据库是在不同机器上运行的两种不相关的技术，这也是可能的。

如果消息传递或数据库事务失败，两者都会中止，因此消息代理可以稍后安全地重新传递消息。因此，通过原子地提交消息及其处理的副作用，我们可以确保消息在效果上*恰好*处理一次，即使在成功之前需要几次重试。中止会丢弃部分完成事务的任何副作用。这被称为*恰好一次语义*。

但是，只有当受事务影响的所有系统都能够使用相同的原子提交协议时，这种分布式事务才有可能。例如，假设处理消息的副作用是发送电子邮件，而电子邮件服务器不支持两阶段提交：如果消息处理失败并重试，可能会发生电子邮件被发送两次或更多次。但是，如果处理消息的所有副作用在事务中止时都会回滚，那么处理步骤可以安全地重试，就好像什么都没有发生一样。

我们将在本章后面回到恰好一次语义的主题。让我们首先看看允许此类异构分布式事务的原子提交协议。

#### XA 事务 {#xa-transactions}

*X/Open XA*（*eXtended Architecture* 的缩写）是跨异构技术实现两阶段提交的标准[^73]。它于 1991 年推出并得到广泛实现：XA 受到许多传统关系数据库（包括 PostgreSQL、MySQL、Db2、SQL Server 和 Oracle）和消息代理（包括 ActiveMQ、HornetQ、MSMQ 和 IBM MQ）的支持。

XA 不是网络协议——它只是用于与事务协调器接口的 C API。此 API 的绑定存在于其他语言中；例如，在 Java EE 应用程序的世界中，XA 事务使用 Java 事务 API（JTA）实现，而 JTA 又由许多使用 Java 数据库连接（JDBC）的数据库驱动程序和使用 Java 消息服务（JMS）API 的消息代理驱动程序支持。

XA 假设你的应用程序使用网络驱动程序或客户端库与参与者数据库或消息服务进行通信。如果驱动程序支持 XA，这意味着它调用 XA API 来确定操作是否应该是分布式事务的一部分——如果是，它将必要的信息发送到数据库服务器。驱动程序还公开回调，协调器可以通过回调要求参与者准备、提交或中止。

事务协调器实现 XA API。该标准没有指定应该如何实现它，但在实践中，协调器通常只是加载到发出事务的应用程序的同一进程中的库（而不是单独的服务）。它跟踪事务中的参与者，在要求他们准备后收集参与者的响应（通过驱动程序的回调），并使用本地磁盘上的日志来跟踪每个事务的提交/中止决定。

如果应用程序进程崩溃，或者运行应用程序的机器死机，协调器也随之消失。任何准备但未提交事务的参与者都陷入存疑。由于协调器的日志在应用程序服务器的本地磁盘上，该服务器必须重新启动，协调器库必须读取日志以恢复每个事务的提交/中止结果。然后，协调器才能使用数据库驱动程序的 XA 回调来要求参与者提交或中止（视情况而定）。数据库服务器无法直接联系协调器，因为所有通信都必须通过其客户端库。

#### 存疑时持有锁 {#holding-locks-while-in-doubt}

为什么我们如此关心事务陷入存疑？系统的其余部分不能继续工作，忽略最终会被清理的存疑事务吗？

问题在于*锁定*。如["读已提交"](#sec_transactions_read_committed)中所讨论的，数据库事务通常对它们修改的任何行进行行级独占锁，以防止脏写。此外，如果你想要可串行化隔离，使用两阶段锁定的数据库还必须对事务*读取*的任何行进行共享锁。

数据库在事务提交或中止之前不能释放这些锁（如[图 8-13](#fig_transactions_two_phase_commit) 中的阴影区域所示）。因此，使用两阶段提交时，事务必须在存疑期间保持锁。如果协调器崩溃并需要 20 分钟才能重新启动，这些锁将保持 20 分钟。如果协调器的日志由于某种原因完全丢失，这些锁将永远保持——或者至少直到管理员手动解决情况。

当这些锁被持有时，没有其他事务可以修改这些行。根据隔离级别，其他事务甚至可能被阻止读取这些行。因此，其他事务不能简单地继续他们的业务——如果他们想要访问相同的数据，他们将被阻塞。这可能导致你的应用程序的大部分变得不可用，直到存疑事务得到解决。

#### 从协调器故障中恢复 {#recovering-from-coordinator-failure}

理论上，如果协调器崩溃并重新启动，它应该从日志中干净地恢复其状态并解决任何存疑事务。但是，在实践中，*孤立的*存疑事务确实会发生[^83] [^84]——也就是说，协调器由于某种原因（例如，由于软件错误导致事务日志丢失或损坏）无法决定结果的事务。这些事务无法自动解决，因此它们永远留在数据库中，持有锁并阻塞其他事务。

即使重新启动数据库服务器也无法解决此问题，因为 2PC 的正确实现必须即使在重新启动时也保留存疑事务的锁（否则它将冒着违反原子性保证的风险）。这是一个棘手的情况。

唯一的出路是管理员手动决定是提交还是回滚事务。管理员必须检查每个存疑事务的参与者，确定是否有任何参与者已经提交或中止，然后将相同的结果应用于其他参与者。解决问题可能需要大量的手动工作，并且很可能需要在严重的生产中断期间在高压力和时间压力下完成（否则，为什么协调器会处于如此糟糕的状态？）。

许多 XA 实现都有一个名为*启发式决策*的紧急逃生舱口：允许参与者在没有协调器明确决定的情况下单方面决定中止或提交存疑事务[^73]。明确地说，这里的*启发式*是*可能破坏原子性*的委婉说法，因为启发式决策违反了两阶段提交中的承诺系统。因此，启发式决策仅用于摆脱灾难性情况，而不用于常规使用。

#### XA 事务的问题 {#problems-with-xa-transactions}

单节点协调器是整个系统的单点故障，使其成为应用程序服务器的一部分也是有问题的，因为协调器在其本地磁盘上的日志成为持久系统状态的关键部分——与数据库本身一样重要。

原则上，XA 事务的协调器可以是高可用和复制的，就像我们对任何其他重要数据库的期望一样。不幸的是，这仍然不能解决 XA 的一个根本问题，即它没有为事务的协调器和参与者提供直接相互通信的方式。它们只能通过调用事务的应用程序代码以及调用参与者的数据库驱动程序进行通信。

即使协调器被复制，应用程序代码也将是单点故障。解决这个问题需要完全重新设计应用程序代码的运行方式，使其复制或可重启，这可能看起来类似于持久执行（参见["持久执行和工作流"](/ch5#sec_encoding_dataflow_workflows)）。但是，实践中似乎没有任何工具实际采用这种方法。

另一个问题是，由于 XA 需要与各种数据系统兼容，它必然是最低公分母。例如，它无法检测跨不同系统的死锁（因为这需要系统交换有关每个事务正在等待的锁的信息的标准化协议），并且它不适用于 SSI（参见["可串行化快照隔离（SSI）"](#sec_transactions_ssi)），因为这需要跨不同系统识别冲突的协议。

这些问题在某种程度上是跨异构技术执行事务所固有的。但是，保持几个异构数据系统彼此一致仍然是一个真实而重要的问题，因此我们需要为其找到不同的解决方案。这可以做到，我们将在下一节和["派生数据与分布式事务"](/ch13#sec_future_derived_vs_transactions)中看到。

### 数据库内部的分布式事务 {#sec_transactions_internal}

如前所述，跨多个异构存储技术的分布式事务与系统内部的分布式事务之间存在很大差异——即，参与节点都是运行相同软件的同一数据库的分片。此类内部分布式事务是"NewSQL"数据库的定义特征，例如 CockroachDB[^5]、TiDB[^6]、Spanner[^7]、FoundationDB[^8] 和 YugabyteDB。某些消息代理（如 Kafka）也支持内部分布式事务[^85]。

这些系统中的许多系统使用两阶段提交来确保写入多个分片的事务的原子性，但它们不会遇到与 XA 事务相同的问题。原因是，由于它们的分布式事务不需要与任何其他技术接口，它们避免了最低公分母陷阱——这些系统的设计者可以自由使用更可靠、更快的更好协议。

XA 的最大问题可以通过以下方式解决：

* 复制协调器，如果主协调器崩溃，自动故障转移到另一个协调器节点；
* 允许协调器和数据分片直接通信，而不通过应用程序代码；
* 复制参与分片，以减少由于分片中的故障而必须中止事务的风险；以及
* 将原子提交协议与支持跨分片死锁检测和一致读取的分布式并发控制协议耦合。

共识算法通常用于复制协调器和数据库分片。我们将在[第 10 章](/ch10#ch_consistency)中看到如何使用共识算法实现分布式事务的原子提交。这些算法通过自动从一个节点故障转移到另一个节点来容忍故障，无需任何人工干预，同时继续保证强一致性属性。

为分布式事务提供的隔离级别取决于系统，但跨分片的快照隔离和可串行化快照隔离都是可能的。有关其工作原理的详细信息，请参见本章末尾引用的论文。

#### 再谈恰好一次消息处理 {#exactly-once-message-processing-revisited}

我们在["恰好一次消息处理"](#sec_transactions_exactly_once)中看到，分布式事务的一个重要用例是确保某些操作恰好生效一次，即使在处理过程中发生崩溃并且需要重试处理。如果你可以跨消息代理和数据库原子地提交事务，则当且仅当成功处理消息并且从处理过程产生的数据库写入被提交时，你可以向代理确认消息。

但是，你实际上不需要这样的分布式事务来实现恰好一次语义。另一种方法如下，它只需要数据库中的事务：

1. 假设每条消息都有唯一的 ID，并且在数据库中有一个已处理消息 ID 的表。当你开始从代理处理消息时，你在数据库上开始一个新事务，并检查消息 ID。如果数据库中已经存在相同的消息 ID，你知道它已经被处理，因此你可以向代理确认消息并丢弃它。
2. 如果消息 ID 尚未在数据库中，你将其添加到表中。然后你处理消息，这可能会导致在同一事务中对数据库进行额外的写入。完成处理消息后，你提交数据库上的事务。
3. 一旦数据库事务成功提交，你就可以向代理确认消息。
4. 一旦消息成功确认给代理，你知道它不会再次尝试处理相同的消息，因此你可以从数据库中删除消息 ID（在单独的事务中）。

如果消息处理器在提交数据库事务之前崩溃，事务将被中止，消息代理将重试处理。如果它在提交后但在向代理确认消息之前崩溃，它也将重试处理，但重试将在数据库中看到消息 ID 并丢弃它。如果它在确认消息后但在从数据库中删除消息 ID 之前崩溃，你将有一个旧的消息 ID 留下，除了占用一点存储空间外不会造成任何伤害。如果在数据库事务中止之前发生重试（如果消息处理器和数据库之间的通信中断，这可能会发生），消息 ID 表上的唯一性约束应该防止两个并发事务插入相同的消息 ID。

因此，实现恰好一次处理只需要数据库中的事务——跨数据库和消息代理的原子性对于此用例不是必需的。在数据库中记录消息 ID 使消息处理具备*幂等性*，因此可以安全地重试消息处理而不会重复其副作用。流处理框架（如 Kafka Streams）中使用类似的方法来实现恰好一次语义，我们将在["容错"](/ch12#sec_stream_fault_tolerance)中看到。

但是，数据库内的内部分布式事务对于此类模式的可伸缩性仍然有用：例如，它们将允许消息 ID 存储在一个分片上，而消息处理更新的主数据存储在其他分片上，并确保跨这些分片的事务提交的原子性。



## 总结 {#summary}

事务是一个抽象层，允许应用程序假装某些并发问题和某些类型的硬件和软件故障不存在。大量错误被简化为简单的*事务中止*，应用程序只需要重试。

在本章中，我们看到了许多事务有助于防止的问题示例。并非所有应用程序都容易受到所有这些问题的影响：具有非常简单的访问模式的应用程序（例如，仅读取和写入单个记录）可能可以在没有事务的情况下管理。但是，对于更复杂的访问模式，事务可以大大减少你需要考虑的潜在错误情况的数量。

没有事务，各种错误场景（进程崩溃、网络中断、停电、磁盘已满、意外并发等）意味着数据可能以各种方式变得不一致。例如，反规范化数据很容易与源数据失去同步。没有事务，很难推理复杂的交互访问对数据库可能产生的影响。

在本章中，我们特别深入地探讨了并发控制的主题。我们讨论了几种广泛使用的隔离级别，特别是*读已提交*、*快照隔离*（有时称为*可重复读*）和*可串行化*。我们通过讨论各种竞态条件的示例来描述这些隔离级别，总结在 [表 8-1](#tab_transactions_isolation_levels) 中：

{{< figure id="tab_transactions_isolation_levels" title="表 8-1. 各种隔离级别可能发生的异常总结" class="w-full my-4" >}}

| 隔离级别 | 脏读   | 读取偏差  | 幻读   | 丢失更新  | 写偏差  |
|------|------|------|------|-------|------|
| 读未提交 | ✗ 可能 | ✗ 可能 | ✗ 可能 | ✗ 可能  | ✗ 可能 |
| 读已提交 | ✓ 防止 | ✗ 可能 | ✗ 可能 | ✗ 可能  | ✗ 可能 |
| 快照隔离 | ✓ 防止 | ✓ 防止 | ✓ 防止 | ? 视情况 | ✗ 可能 |
| 可串行化 | ✓ 防止 | ✓ 防止 | ✓ 防止 | ✓ 防止  | ✓ 防止 |

脏读
: 一个客户端在另一个客户端的写入提交之前读取它们。读已提交隔离级别和更强的级别防止脏读。

脏写
: 一个客户端覆盖另一个客户端已写入但尚未提交的数据。几乎所有事务实现都防止脏写。

读取偏差
: 客户端在不同时间点看到数据库的不同部分。某些读取偏差的情况也称为*不可重复读*。这个问题最常通过快照隔离来防止，它允许事务从对应于特定时间点的一致快照读取。它通常使用*多版本并发控制*（MVCC）实现。

丢失更新
: 两个客户端并发执行读-修改-写循环。一个覆盖另一个的写入而不合并其更改，因此数据丢失。某些快照隔离的实现会自动防止此异常，而其他实现需要手动锁（`SELECT FOR UPDATE`）。

写偏差
: 事务读取某些内容，根据它看到的值做出决定，并将决定写入数据库。但是，在进行写入时，决策的前提不再为真。只有可串行化隔离才能防止此异常。

幻读
: 事务读取匹配某些搜索条件的对象。另一个客户端进行影响该搜索结果的写入。快照隔离防止直接的幻读，但写偏差上下文中的幻读需要特殊处理，例如索引范围锁。

弱隔离级别可以防止某些异常，但让你（应用程序开发人员）手动处理其他异常（例如，使用显式锁定）。只有可串行化隔离可以防止所有这些问题。我们讨论了实现可串行化事务的三种不同方法：

字面上串行执行事务
: 如果你可以使每个事务执行得非常快（通常通过使用存储过程），并且事务吞吐量足够低，可以在单个 CPU 核心上处理或可以分片，这是一个简单有效的选择。

两阶段锁定
: 几十年来，这一直是实现可串行化的标准方法，但许多应用程序由于其性能不佳而避免使用它。

可串行化快照隔离（SSI）
: 一种相对较新的算法，避免了前面方法的大部分缺点。它使用乐观方法，允许事务在不阻塞的情况下进行。当事务想要提交时，它会被检查，如果执行不可串行化，它将被中止。

最后，我们研究了当事务分布在多个节点上时如何实现原子性，使用两阶段提交。如果这些节点都运行相同的数据库软件，分布式事务可以很好地工作，但跨不同存储技术（使用 XA 事务），2PC 是有问题的：它对协调器和驱动事务的应用程序代码中的故障非常敏感，并且与并发控制机制的交互很差。幸运的是，幂等性可以确保恰好一次语义，而无需跨不同存储技术的原子提交，我们将在后面的章节中看到更多相关内容。

本章中的示例使用了关系数据模型。但是，如["多对象事务的需求"](#sec_transactions_need)中所讨论的，无论使用哪种数据模型，事务都是有价值的数据库功能。



### 参考


[^1]: Steven J. Murdoch. [What went wrong with Horizon: learning from the Post Office Trial](https://www.benthamsgaze.org/2021/07/15/what-went-wrong-with-horizon-learning-from-the-post-office-trial/). *benthamsgaze.org*, July 2021. Archived at [perma.cc/CNM4-553F](https://perma.cc/CNM4-553F)
[^2]: Donald D. Chamberlin, Morton M. Astrahan, Michael W. Blasgen, James N. Gray, W. Frank King, Bruce G. Lindsay, Raymond Lorie, James W. Mehl, Thomas G. Price, Franco Putzolu, Patricia Griffiths Selinger, Mario Schkolnick, Donald R. Slutz, Irving L. Traiger, Bradford W. Wade, and Robert A. Yost. [A History and Evaluation of System R](https://dsf.berkeley.edu/cs262/2005/SystemR.pdf). *Communications of the ACM*, volume 24, issue 10, pages 632–646, October 1981. [doi:10.1145/358769.358784](https://doi.org/10.1145/358769.358784)
[^3]: Jim N. Gray, Raymond A. Lorie, Gianfranco R. Putzolu, and Irving L. Traiger. [Granularity of Locks and Degrees of Consistency in a Shared Data Base](https://citeseerx.ist.psu.edu/pdf/e127f0a6a912bb9150ecfe03c0ebf7fbc289a023). in *Modelling in Data Base Management Systems: Proceedings of the IFIP Working Conference on Modelling in Data Base Management Systems*, edited by G. M. Nijssen, pages 364–394, Elsevier/North Holland Publishing, 1976. Also in *Readings in Database Systems*, 4th edition, edited by Joseph M. Hellerstein and Michael Stonebraker, MIT Press, 2005. ISBN: 978-0-262-69314-1
[^4]: Kapali P. Eswaran, Jim N. Gray, Raymond A. Lorie, and Irving L. Traiger. [The Notions of Consistency and Predicate Locks in a Database System](https://jimgray.azurewebsites.net/papers/On%20the%20Notions%20of%20Consistency%20and%20Predicate%20Locks%20in%20a%20Database%20System%20CACM.pdf?from=https://research.microsoft.com/en-us/um/people/gray/papers/On%20the%20Notions%20of%20Consistency%20and%20Predicate%20Locks%20in%20a%20Database%20System%20CACM.pdf). *Communications of the ACM*, volume 19, issue 11, pages 624–633, November 1976. [doi:10.1145/360363.360369](https://doi.org/10.1145/360363.360369)
[^5]: Rebecca Taft, Irfan Sharif, Andrei Matei, Nathan VanBenschoten, Jordan Lewis, Tobias Grieger, Kai Niemi, Andy Woods, Anne Birzin, Raphael Poss, Paul Bardea, Amruta Ranade, Ben Darnell, Bram Gruneir, Justin Jaffray, Lucy Zhang, and Peter Mattis. [CockroachDB: The Resilient Geo-Distributed SQL Database](https://dl.acm.org/doi/pdf/10.1145/3318464.3386134). At *ACM SIGMOD International Conference on Management of Data* (SIGMOD), pages 1493–1509, June 2020. [doi:10.1145/3318464.3386134](https://doi.org/10.1145/3318464.3386134)
[^6]: Dongxu Huang, Qi Liu, Qiu Cui, Zhuhe Fang, Xiaoyu Ma, Fei Xu, Li Shen, Liu Tang, Yuxing Zhou, Menglong Huang, Wan Wei, Cong Liu, Jian Zhang, Jianjun Li, Xuelian Wu, Lingyu Song, Ruoxi Sun, Shuaipeng Yu, Lei Zhao, Nicholas Cameron, Liquan Pei, and Xin Tang. [TiDB: a Raft-based HTAP database](https://www.vldb.org/pvldb/vol13/p3072-huang.pdf). *Proceedings of the VLDB Endowment*, volume 13, issue 12, pages 3072–3084. [doi:10.14778/3415478.3415535](https://doi.org/10.14778/3415478.3415535)
[^7]: James C. Corbett, Jeffrey Dean, Michael Epstein, Andrew Fikes, Christopher Frost, JJ Furman, Sanjay Ghemawat, Andrey Gubarev, Christopher Heiser, Peter Hochschild, Wilson Hsieh, Sebastian Kanthak, Eugene Kogan, Hongyi Li, Alexander Lloyd, Sergey Melnik, David Mwaura, David Nagle, Sean Quinlan, Rajesh Rao, Lindsay Rolig, Dale Woodford, Yasushi Saito, Christopher Taylor, Michal Szymaniak, and Ruth Wang. [Spanner: Google’s Globally-Distributed Database](https://research.google/pubs/pub39966/). At *10th USENIX Symposium on Operating System Design and Implementation* (OSDI), October 2012.
[^8]: Jingyu Zhou, Meng Xu, Alexander Shraer, Bala Namasivayam, Alex Miller, Evan Tschannen, Steve Atherton, Andrew J. Beamon, Rusty Sears, John Leach, Dave Rosenthal, Xin Dong, Will Wilson, Ben Collins, David Scherer, Alec Grieser, Young Liu, Alvin Moore, Bhaskar Muppana, Xiaoge Su, and Vishesh Yadav. [FoundationDB: A Distributed Unbundled Transactional Key Value Store](https://www.foundationdb.org/files/fdb-paper.pdf). At *ACM International Conference on Management of Data* (SIGMOD), June 2021. [doi:10.1145/3448016.3457559](https://doi.org/10.1145/3448016.3457559)
[^9]: Theo Härder and Andreas Reuter. [Principles of Transaction-Oriented Database Recovery](https://citeseerx.ist.psu.edu/pdf/11ef7c142295aeb1a28a0e714c91fc8d610c3047). *ACM Computing Surveys*, volume 15, issue 4, pages 287–317, December 1983. [doi:10.1145/289.291](https://doi.org/10.1145/289.291)
[^10]: Peter Bailis, Alan Fekete, Ali Ghodsi, Joseph M. Hellerstein, and Ion Stoica. [HAT, not CAP: Towards Highly Available Transactions](https://www.usenix.org/system/files/conference/hotos13/hotos13-final80.pdf). At *14th USENIX Workshop on Hot Topics in Operating Systems* (HotOS), May 2013.
[^11]: Armando Fox, Steven D. Gribble, Yatin Chawathe, Eric A. Brewer, and Paul Gauthier. [Cluster-Based Scalable Network Services](https://people.eecs.berkeley.edu/~brewer/cs262b/TACC.pdf). At *16th ACM Symposium on Operating Systems Principles* (SOSP), October 1997. [doi:10.1145/268998.266662](https://doi.org/10.1145/268998.266662)
[^12]: Tony Andrews. [Enforcing Complex Constraints in Oracle](https://tonyandrews.blogspot.com/2004/10/enforcing-complex-constraints-in.html). *tonyandrews.blogspot.co.uk*, October 2004. Archived at [archive.org](https://web.archive.org/web/20220201190625/https%3A//tonyandrews.blogspot.com/2004/10/enforcing-complex-constraints-in.html)
[^13]: Philip A. Bernstein, Vassos Hadzilacos, and Nathan Goodman. [*Concurrency Control and Recovery in Database Systems*](https://www.microsoft.com/en-us/research/people/philbe/book/). Addison-Wesley, 1987. ISBN: 978-0-201-10715-9, available online at [*microsoft.com*](https://www.microsoft.com/en-us/research/people/philbe/book/).
[^14]: Alan Fekete, Dimitrios Liarokapis, Elizabeth O’Neil, Patrick O’Neil, and Dennis Shasha. [Making Snapshot Isolation Serializable](https://www.cse.iitb.ac.in/infolab/Data/Courses/CS632/2009/Papers/p492-fekete.pdf). *ACM Transactions on Database Systems*, volume 30, issue 2, pages 492–528, June 2005. [doi:10.1145/1071610.1071615](https://doi.org/10.1145/1071610.1071615)
[^15]: Mai Zheng, Joseph Tucek, Feng Qin, and Mark Lillibridge. [Understanding the Robustness of SSDs Under Power Fault](https://www.usenix.org/system/files/conference/fast13/fast13-final80.pdf). At *11th USENIX Conference on File and Storage Technologies* (FAST), February 2013.
[^16]: Laurie Denness. [SSDs: A Gift and a Curse](https://laur.ie/blog/2015/06/ssds-a-gift-and-a-curse/). *laur.ie*, June 2015. Archived at [perma.cc/6GLP-BX3T](https://perma.cc/6GLP-BX3T)
[^17]: Adam Surak. [When Solid State Drives Are Not That Solid](https://www.algolia.com/blog/engineering/when-solid-state-drives-are-not-that-solid). *blog.algolia.com*, June 2015. Archived at [perma.cc/CBR9-QZEE](https://perma.cc/CBR9-QZEE)
[^18]: Hewlett Packard Enterprise. [Bulletin: (Revision) HPE SAS Solid State Drives - Critical Firmware Upgrade Required for Certain HPE SAS Solid State Drive Models to Prevent Drive Failure at 32,768 Hours of Operation](https://support.hpe.com/hpesc/public/docDisplay?docId=emr_na-a00092491en_us). *support.hpe.com*, November 2019. Archived at [perma.cc/CZR4-AQBS](https://perma.cc/CZR4-AQBS)
[^19]: Craig Ringer et al. [PostgreSQL’s handling of fsync() errors is unsafe and risks data loss at least on XFS](https://www.postgresql.org/message-id/flat/CAMsr%2BYHh%2B5Oq4xziwwoEfhoTZgr07vdGG%2Bhu%3D1adXx59aTeaoQ%40mail.gmail.com). Email thread on pgsql-hackers mailing list, *postgresql.org*, March 2018. Archived at [perma.cc/5RKU-57FL](https://perma.cc/5RKU-57FL)
[^20]: Anthony Rebello, Yuvraj Patel, Ramnatthan Alagappan, Andrea C. Arpaci-Dusseau, and Remzi H. Arpaci-Dusseau. [Can Applications Recover from fsync Failures?](https://www.usenix.org/conference/atc20/presentation/rebello) At *USENIX Annual Technical Conference* (ATC), July 2020.
[^21]: Thanumalayan Sankaranarayana Pillai, Vijay Chidambaram, Ramnatthan Alagappan, Samer Al-Kiswany, Andrea C. Arpaci-Dusseau, and Remzi H. Arpaci-Dusseau. [Crash Consistency: Rethinking the Fundamental Abstractions of the File System](https://dl.acm.org/doi/pdf/10.1145/2800695.2801719). *ACM Queue*, volume 13, issue 7, pages 20–28, July 2015. [doi:10.1145/2800695.2801719](https://doi.org/10.1145/2800695.2801719)
[^22]: Thanumalayan Sankaranarayana Pillai, Vijay Chidambaram, Ramnatthan Alagappan, Samer Al-Kiswany, Andrea C. Arpaci-Dusseau, and Remzi H. Arpaci-Dusseau. [All File Systems Are Not Created Equal: On the Complexity of Crafting Crash-Consistent Applications](https://www.usenix.org/system/files/conference/osdi14/osdi14-paper-pillai.pdf). At *11th USENIX Symposium on Operating Systems Design and Implementation* (OSDI), October 2014.
[^23]: Chris Siebenmann. [Unix’s File Durability Problem](https://utcc.utoronto.ca/~cks/space/blog/unix/FileSyncProblem). *utcc.utoronto.ca*, April 2016. Archived at [perma.cc/VSS8-5MC4](https://perma.cc/VSS8-5MC4)
[^24]: Aishwarya Ganesan, Ramnatthan Alagappan, Andrea C. Arpaci-Dusseau, and Remzi H. Arpaci-Dusseau. [Redundancy Does Not Imply Fault Tolerance: Analysis of Distributed Storage Reactions to Single Errors and Corruptions](https://www.usenix.org/conference/fast17/technical-sessions/presentation/ganesan). At *15th USENIX Conference on File and Storage Technologies* (FAST), February 2017.
[^25]: Lakshmi N. Bairavasundaram, Garth R. Goodson, Bianca Schroeder, Andrea C. Arpaci-Dusseau, and Remzi H. Arpaci-Dusseau. [An Analysis of Data Corruption in the Storage Stack](https://www.usenix.org/legacy/event/fast08/tech/full_papers/bairavasundaram/bairavasundaram.pdf). At *6th USENIX Conference on File and Storage Technologies* (FAST), February 2008.
[^26]: Bianca Schroeder, Raghav Lagisetty, and Arif Merchant. [Flash Reliability in Production: The Expected and the Unexpected](https://www.usenix.org/conference/fast16/technical-sessions/presentation/schroeder). At *14th USENIX Conference on File and Storage Technologies* (FAST), February 2016.
[^27]: Don Allison. [SSD Storage – Ignorance of Technology Is No Excuse](https://blog.korelogic.com/blog/2015/03/24). *blog.korelogic.com*, March 2015. Archived at [perma.cc/9QN4-9SNJ](https://perma.cc/9QN4-9SNJ)
[^28]: Gordon Mah Ung. [Debunked: Your SSD won’t lose data if left unplugged after all](https://www.pcworld.com/article/427602/debunked-your-ssd-wont-lose-data-if-left-unplugged-after-all.html). *pcworld.com*, May 2015. Archived at [perma.cc/S46H-JUDU](https://perma.cc/S46H-JUDU)
[^29]: Martin Kleppmann. [Hermitage: Testing the ‘I’ in ACID](https://martin.kleppmann.com/2014/11/25/hermitage-testing-the-i-in-acid.html). *martin.kleppmann.com*, November 2014. Archived at [perma.cc/KP2Y-AQGK](https://perma.cc/KP2Y-AQGK)
[^30]: Todd Warszawski and Peter Bailis. [ACIDRain: Concurrency-Related Attacks on Database-Backed Web Applications](http://www.bailis.org/papers/acidrain-sigmod2017.pdf). At *ACM International Conference on Management of Data* (SIGMOD), May 2017. [doi:10.1145/3035918.3064037](https://doi.org/10.1145/3035918.3064037)
[^31]: Tristan D’Agosta. [BTC Stolen from Poloniex](https://bitcointalk.org/index.php?topic=499580). *bitcointalk.org*, March 2014. Archived at [perma.cc/YHA6-4C5D](https://perma.cc/YHA6-4C5D)
[^32]: bitcointhief2. [How I Stole Roughly 100 BTC from an Exchange and How I Could Have Stolen More!](https://www.reddit.com/r/Bitcoin/comments/1wtbiu/how_i_stole_roughly_100_btc_from_an_exchange_and/) *reddit.com*, February 2014. Archived at [archive.org](https://web.archive.org/web/20250118042610/https%3A//www.reddit.com/r/Bitcoin/comments/1wtbiu/how_i_stole_roughly_100_btc_from_an_exchange_and/)
[^33]: Sudhir Jorwekar, Alan Fekete, Krithi Ramamritham, and S. Sudarshan. [Automating the Detection of Snapshot Isolation Anomalies](https://www.vldb.org/conf/2007/papers/industrial/p1263-jorwekar.pdf). At *33rd International Conference on Very Large Data Bases* (VLDB), September 2007.
[^34]: Michael Melanson. [Transactions: The Limits of Isolation](https://www.michaelmelanson.net/posts/transactions-the-limits-of-isolation/). *michaelmelanson.net*, November 2014. Archived at [perma.cc/RG5R-KMYZ](https://perma.cc/RG5R-KMYZ)
[^35]: Edward Kim. [How ACH works: A developer perspective — Part 1](https://engineering.gusto.com/how-ach-works-a-developer-perspective-part-1-339d3e7bea1). *engineering.gusto.com*, April 2014. Archived at [perma.cc/7B2H-PU94](https://perma.cc/7B2H-PU94)
[^36]: Hal Berenson, Philip A. Bernstein, Jim N. Gray, Jim Melton, Elizabeth O’Neil, and Patrick O’Neil. [A Critique of ANSI SQL Isolation Levels](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-95-51.pdf). At *ACM International Conference on Management of Data* (SIGMOD), May 1995. [doi:10.1145/568271.223785](https://doi.org/10.1145/568271.223785)
[^37]: Atul Adya. [Weak Consistency: A Generalized Theory and Optimistic Implementations for Distributed Transactions](https://pmg.csail.mit.edu/papers/adya-phd.pdf). PhD Thesis, Massachusetts Institute of Technology, March 1999. Archived at [perma.cc/E97M-HW5Q](https://perma.cc/E97M-HW5Q)
[^38]: Peter Bailis, Aaron Davidson, Alan Fekete, Ali Ghodsi, Joseph M. Hellerstein, and Ion Stoica. [Highly Available Transactions: Virtues and Limitations](https://www.vldb.org/pvldb/vol7/p181-bailis.pdf). At *40th International Conference on Very Large Data Bases* (VLDB), September 2014.
[^39]: Natacha Crooks, Youer Pu, Lorenzo Alvisi, and Allen Clement. [Seeing is Believing: A Client-Centric Specification of Database Isolation](https://www.cs.cornell.edu/lorenzo/papers/Crooks17Seeing.pdf). At *ACM Symposium on Principles of Distributed Computing* (PODC), pages 73–82, July 2017. [doi:10.1145/3087801.3087802](https://doi.org/10.1145/3087801.3087802)
[^40]: Bruce Momjian. [MVCC Unmasked](https://momjian.us/main/writings/pgsql/mvcc.pdf). *momjian.us*, July 2014. Archived at [perma.cc/KQ47-9GYB](https://perma.cc/KQ47-9GYB)
[^41]: Peter Alvaro and Kyle Kingsbury. [MySQL 8.0.34](https://jepsen.io/analyses/mysql-8.0.34). *jepsen.io*, December 2023. Archived at [perma.cc/HGE2-Z878](https://perma.cc/HGE2-Z878)
[^42]: Egor Rogov. [PostgreSQL 14 Internals](https://postgrespro.com/community/books/internals). *postgrespro.com*, April 2023. Archived at [perma.cc/FRK2-D7WB](https://perma.cc/FRK2-D7WB)
[^43]: Hironobu Suzuki. [The Internals of PostgreSQL](https://www.interdb.jp/pg/). *interdb.jp*, 2017.
[^44]: Rohan Reddy Alleti. [Internals of MVCC in Postgres: Hidden costs of Updates vs Inserts](https://medium.com/%40rohanjnr44/internals-of-mvcc-in-postgres-hidden-costs-of-updates-vs-inserts-381eadd35844). *medium.com*, March 2025. Archived at [perma.cc/3ACX-DFXT](https://perma.cc/3ACX-DFXT)
[^45]: Andy Pavlo and Bohan Zhang. [The Part of PostgreSQL We Hate the Most](https://www.cs.cmu.edu/~pavlo/blog/2023/04/the-part-of-postgresql-we-hate-the-most.html). *cs.cmu.edu*, April 2023. Archived at [perma.cc/XSP6-3JBN](https://perma.cc/XSP6-3JBN)
[^46]: Yingjun Wu, Joy Arulraj, Jiexi Lin, Ran Xian, and Andrew Pavlo. [An empirical evaluation of in-memory multi-version concurrency control](https://vldb.org/pvldb/vol10/p781-Wu.pdf). *Proceedings of the VLDB Endowment*, volume 10, issue 7, pages 781–792, March 2017. [doi:10.14778/3067421.3067427](https://doi.org/10.14778/3067421.3067427)
[^47]: Nikita Prokopov. [Unofficial Guide to Datomic Internals](https://tonsky.me/blog/unofficial-guide-to-datomic-internals/). *tonsky.me*, May 2014.
[^48]: Daniil Svetlov. [A Practical Guide to Taming Postgres Isolation Anomalies](https://dansvetlov.me/postgres-anomalies/). *dansvetlov.me*, March 2025. Archived at [perma.cc/L7LE-TDLS](https://perma.cc/L7LE-TDLS)
[^49]: Nate Wiger. [An Atomic Rant](https://nateware.com/2010/02/18/an-atomic-rant/). *nateware.com*, February 2010. Archived at [perma.cc/5ZYB-PE44](https://perma.cc/5ZYB-PE44)
[^50]: James Coglan. [Reading and writing, part 3: web applications](https://blog.jcoglan.com/2020/10/12/reading-and-writing-part-3/). *blog.jcoglan.com*, October 2020. Archived at [perma.cc/A7EK-PJVS](https://perma.cc/A7EK-PJVS)
[^51]: Peter Bailis, Alan Fekete, Michael J. Franklin, Ali Ghodsi, Joseph M. Hellerstein, and Ion Stoica. [Feral Concurrency Control: An Empirical Investigation of Modern Application Integrity](http://www.bailis.org/papers/feral-sigmod2015.pdf). At *ACM International Conference on Management of Data* (SIGMOD), June 2015. [doi:10.1145/2723372.2737784](https://doi.org/10.1145/2723372.2737784)
[^52]: Jaana Dogan. [Things I Wished More Developers Knew About Databases](https://rakyll.medium.com/things-i-wished-more-developers-knew-about-databases-2d0178464f78). *rakyll.medium.com*, April 2020. Archived at [perma.cc/6EFK-P2TD](https://perma.cc/6EFK-P2TD)
[^53]: Michael J. Cahill, Uwe Röhm, and Alan Fekete. [Serializable Isolation for Snapshot Databases](https://www.cs.cornell.edu/~sowell/dbpapers/serializable_isolation.pdf). At *ACM International Conference on Management of Data* (SIGMOD), June 2008. [doi:10.1145/1376616.1376690](https://doi.org/10.1145/1376616.1376690)
[^54]: Dan R. K. Ports and Kevin Grittner. [Serializable Snapshot Isolation in PostgreSQL](https://drkp.net/papers/ssi-vldb12.pdf). At *38th International Conference on Very Large Databases* (VLDB), August 2012.
[^55]: Douglas B. Terry, Marvin M. Theimer, Karin Petersen, Alan J. Demers, Mike J. Spreitzer and Carl H. Hauser. [Managing Update Conflicts in Bayou, a Weakly Connected Replicated Storage System](https://pdos.csail.mit.edu/6.824/papers/bayou-conflicts.pdf). At *15th ACM Symposium on Operating Systems Principles* (SOSP), December 1995. [doi:10.1145/224056.224070](https://doi.org/10.1145/224056.224070)
[^56]: Hans-Jürgen Schönig. [Constraints over multiple rows in PostgreSQL](https://www.cybertec-postgresql.com/en/postgresql-constraints-over-multiple-rows/). *cybertec-postgresql.com*, June 2021. Archived at [perma.cc/2TGH-XUPZ](https://perma.cc/2TGH-XUPZ)
[^57]: Michael Stonebraker, Samuel Madden, Daniel J. Abadi, Stavros Harizopoulos, Nabil Hachem, and Pat Helland. [The End of an Architectural Era (It’s Time for a Complete Rewrite)](https://vldb.org/conf/2007/papers/industrial/p1150-stonebraker.pdf). At *33rd International Conference on Very Large Data Bases* (VLDB), September 2007.
[^58]: John Hugg. [H-Store/VoltDB Architecture vs. CEP Systems and Newer Streaming Architectures](https://www.youtube.com/watch?v=hD5M4a1UVz8). At *Data @Scale Boston*, November 2014.
[^59]: Robert Kallman, Hideaki Kimura, Jonathan Natkins, Andrew Pavlo, Alexander Rasin, Stanley Zdonik, Evan P. C. Jones, Samuel Madden, Michael Stonebraker, Yang Zhang, John Hugg, and Daniel J. Abadi. [H-Store: A High-Performance, Distributed Main Memory Transaction Processing System](https://www.vldb.org/pvldb/vol1/1454211.pdf). *Proceedings of the VLDB Endowment*, volume 1, issue 2, pages 1496–1499, August 2008.
[^60]: Rich Hickey. [The Architecture of Datomic](https://www.infoq.com/articles/Architecture-Datomic/). *infoq.com*, November 2012. Archived at [perma.cc/5YWU-8XJK](https://perma.cc/5YWU-8XJK)
[^61]: John Hugg. [Debunking Myths About the VoltDB In-Memory Database](https://dzone.com/articles/debunking-myths-about-voltdb). *dzone.com*, May 2014. Archived at [perma.cc/2Z9N-HPKF](https://perma.cc/2Z9N-HPKF)
[^62]: Xinjing Zhou, Viktor Leis, Xiangyao Yu, and Michael Stonebraker. [OLTP Through the Looking Glass 16 Years Later: Communication is the New Bottleneck](https://www.vldb.org/cidrdb/papers/2025/p17-zhou.pdf). At *15th Annual Conference on Innovative Data Systems Research* (CIDR), January 2025.
[^63]: Xinjing Zhou, Xiangyao Yu, Goetz Graefe, and Michael Stonebraker. [Lotus: scalable multi-partition transactions on single-threaded partitioned databases](https://www.vldb.org/pvldb/vol15/p2939-zhou.pdf). *Proceedings of the VLDB Endowment* (PVLDB), volume 15, issue 11, pages 2939–2952, July 2022. [doi:10.14778/3551793.3551843](https://doi.org/10.14778/3551793.3551843)
[^64]: Joseph M. Hellerstein, Michael Stonebraker, and James Hamilton. [Architecture of a Database System](https://dsf.berkeley.edu/papers/fntdb07-architecture.pdf). *Foundations and Trends in Databases*, volume 1, issue 2, pages 141–259, November 2007. [doi:10.1561/1900000002](https://doi.org/10.1561/1900000002)
[^65]: Michael J. Cahill. [Serializable Isolation for Snapshot Databases](https://ses.library.usyd.edu.au/bitstream/handle/2123/5353/michael-cahill-2009-thesis.pdf). PhD Thesis, University of Sydney, July 2009. Archived at [perma.cc/727J-NTMP](https://perma.cc/727J-NTMP)
[^66]: Cristian Diaconu, Craig Freedman, Erik Ismert, Per-Åke Larson, Pravin Mittal, Ryan Stonecipher, Nitin Verma, and Mike Zwilling. [Hekaton: SQL Server’s Memory-Optimized OLTP Engine](https://www.microsoft.com/en-us/research/wp-content/uploads/2013/06/Hekaton-Sigmod2013-final.pdf). At *ACM SIGMOD International Conference on Management of Data* (SIGMOD), pages 1243–1254, June 2013. [doi:10.1145/2463676.2463710](https://doi.org/10.1145/2463676.2463710)
[^67]: Thomas Neumann, Tobias Mühlbauer, and Alfons Kemper. [Fast Serializable Multi-Version Concurrency Control for Main-Memory Database Systems](https://db.in.tum.de/~muehlbau/papers/mvcc.pdf). At *ACM SIGMOD International Conference on Management of Data* (SIGMOD), pages 677–689, May 2015. [doi:10.1145/2723372.2749436](https://doi.org/10.1145/2723372.2749436)
[^68]: D. Z. Badal. [Correctness of Concurrency Control and Implications in Distributed Databases](https://ieeexplore.ieee.org/abstract/document/762563). At *3rd International IEEE Computer Software and Applications Conference* (COMPSAC), November 1979. [doi:10.1109/CMPSAC.1979.762563](https://doi.org/10.1109/CMPSAC.1979.762563)
[^69]: Rakesh Agrawal, Michael J. Carey, and Miron Livny. [Concurrency Control Performance Modeling: Alternatives and Implications](https://people.eecs.berkeley.edu/~brewer/cs262/ConcControl.pdf). *ACM Transactions on Database Systems* (TODS), volume 12, issue 4, pages 609–654, December 1987. [doi:10.1145/32204.32220](https://doi.org/10.1145/32204.32220)
[^70]: Marc Brooker. [Snapshot Isolation vs Serializability](https://brooker.co.za/blog/2024/12/17/occ-and-isolation.html). *brooker.co.za*, December 2024. Archived at [perma.cc/5TRC-CR5G](https://perma.cc/5TRC-CR5G)
[^71]: B. G. Lindsay, P. G. Selinger, C. Galtieri, J. N. Gray, R. A. Lorie, T. G. Price, F. Putzolu, I. L. Traiger, and B. W. Wade. [Notes on Distributed Databases](https://dominoweb.draco.res.ibm.com/reports/RJ2571.pdf). IBM Research, Research Report RJ2571(33471), July 1979. Archived at [perma.cc/EPZ3-MHDD](https://perma.cc/EPZ3-MHDD)
[^72]: C. Mohan, Bruce G. Lindsay, and Ron Obermarck. [Transaction Management in the R\* Distributed Database Management System](https://cs.brown.edu/courses/csci2270/archives/2012/papers/dtxn/p378-mohan.pdf). *ACM Transactions on Database Systems*, volume 11, issue 4, pages 378–396, December 1986. [doi:10.1145/7239.7266](https://doi.org/10.1145/7239.7266)
[^73]: X/Open Company Ltd. [Distributed Transaction Processing: The XA Specification](https://pubs.opengroup.org/onlinepubs/009680699/toc.pdf). Technical Standard XO/CAE/91/300, December 1991. ISBN: 978-1-872-63024-3, archived at [perma.cc/Z96H-29JB](https://perma.cc/Z96H-29JB)
[^74]: Ivan Silva Neto and Francisco Reverbel. [Lessons Learned from Implementing WS-Coordination and WS-AtomicTransaction](https://www.ime.usp.br/~reverbel/papers/icis2008.pdf). At *7th IEEE/ACIS International Conference on Computer and Information Science* (ICIS), May 2008. [doi:10.1109/ICIS.2008.75](https://doi.org/10.1109/ICIS.2008.75)
[^75]: James E. Johnson, David E. Langworthy, Leslie Lamport, and Friedrich H. Vogt. [Formal Specification of a Web Services Protocol](https://www.microsoft.com/en-us/research/publication/formal-specification-of-a-web-services-protocol/). At *1st International Workshop on Web Services and Formal Methods* (WS-FM), February 2004. [doi:10.1016/j.entcs.2004.02.022](https://doi.org/10.1016/j.entcs.2004.02.022)
[^76]: Jim Gray. [The Transaction Concept: Virtues and Limitations](https://jimgray.azurewebsites.net/papers/thetransactionconcept.pdf). At *7th International Conference on Very Large Data Bases* (VLDB), September 1981.
[^77]: Dale Skeen. [Nonblocking Commit Protocols](https://www.cs.utexas.edu/~lorenzo/corsi/cs380d/papers/Ske81.pdf). At *ACM International Conference on Management of Data* (SIGMOD), April 1981. [doi:10.1145/582318.582339](https://doi.org/10.1145/582318.582339)
[^78]: Gregor Hohpe. [Your Coffee Shop Doesn’t Use Two-Phase Commit](https://www.martinfowler.com/ieeeSoftware/coffeeShop.pdf). *IEEE Software*, volume 22, issue 2, pages 64–66, March 2005. [doi:10.1109/MS.2005.52](https://doi.org/10.1109/MS.2005.52)
[^79]: Pat Helland. [Life Beyond Distributed Transactions: An Apostate’s Opinion](https://www.cidrdb.org/cidr2007/papers/cidr07p15.pdf). At *3rd Biennial Conference on Innovative Data Systems Research* (CIDR), January 2007.
[^80]: Jonathan Oliver. [My Beef with MSDTC and Two-Phase Commits](https://blog.jonathanoliver.com/my-beef-with-msdtc-and-two-phase-commits/). *blog.jonathanoliver.com*, April 2011. Archived at [perma.cc/K8HF-Z4EN](https://perma.cc/K8HF-Z4EN)
[^81]: Oren Eini (Ahende Rahien). [The Fallacy of Distributed Transactions](https://ayende.com/blog/167362/the-fallacy-of-distributed-transactions). *ayende.com*, July 2014. Archived at [perma.cc/VB87-2JEF](https://perma.cc/VB87-2JEF)
[^82]: Clemens Vasters. [Transactions in Windows Azure (with Service Bus) – An Email Discussion](https://learn.microsoft.com/en-gb/archive/blogs/clemensv/transactions-in-windows-azure-with-service-bus-an-email-discussion). *learn.microsoft.com*, July 2012. Archived at [perma.cc/4EZ9-5SKW](https://perma.cc/4EZ9-5SKW)
[^83]: Ajmer Dhariwal. [Orphaned MSDTC Transactions (-2 spids)](https://www.eraofdata.com/posts/2008/orphaned-msdtc-transactions-2-spids/). *eraofdata.com*, December 2008. Archived at [perma.cc/YG6F-U34C](https://perma.cc/YG6F-U34C)
[^84]: Paul Randal. [Real World Story of DBCC PAGE Saving the Day](https://www.sqlskills.com/blogs/paul/real-world-story-of-dbcc-page-saving-the-day/). *sqlskills.com*, June 2013. Archived at [perma.cc/2MJN-A5QH](https://perma.cc/2MJN-A5QH)
[^85]: Guozhang Wang, Lei Chen, Ayusman Dikshit, Jason Gustafson, Boyang Chen, Matthias J. Sax, John Roesler, Sophie Blee-Goldman, Bruno Cadonna, Apurva Mehta, Varun Madan, and Jun Rao. [Consistency and Completeness: Rethinking Distributed Stream Processing in Apache Kafka](https://dl.acm.org/doi/pdf/10.1145/3448016.3457556). At *ACM International Conference on Management of Data* (SIGMOD), June 2021. [doi:10.1145/3448016.3457556](https://doi.org/10.1145/3448016.3457556)
