ch7 初翻35%

This commit is contained in:
Vonng 2018-02-11 02:36:33 +08:00
parent 6d4376fa22
commit 436a7e4dce
3 changed files with 157 additions and 127 deletions

View File

@ -69,7 +69,7 @@
| 第二部分:分布式数据——概览 | [part-ii.md](part-ii.md) | | 初翻 |
| 第五章:复制 | [ch5.md](ch5.md) | | 初翻 |
| 第六章:分片 | [ch6.md](ch6.md) | | 初翻 |
| 第七章:事务 | [ch7.md](ch7.md) | | 机翻 |
| 第七章:事务 | [ch7.md](ch7.md) | | 初翻 35% |
| 第八章:分布式系统的麻烦 | [ch8.md](ch8.md) | | - |
| 第九章:一致性与共识 | [ch9.md](ch9.md) | | - |
| 第三部分:前言 | [part-iii.md](part-iii.md) | | 机翻 |

View File

@ -516,8 +516,10 @@ Twitter的第一个版本使用了方法1但系统努力跟上主页时间线
[doi:10.1109/COMPSAC.2008.50](http://dx.doi.org/10.1109/COMPSAC.2008.50)
c
| | | |
| ---- | ---- | ---- |
| | | |
------
| 上一章 | 目录 | 下一章 |
| ----------------------------------- | ------------------------------- | ------------------------------------ |
| [第一部分:数据系统基础](part-i.md) | [设计数据密集型应用](README.md) | [第二章:数据模型与查询语言](ch2.md) |

View File

@ -2,7 +2,7 @@
![](img/ch7.png)
> 一些作者声称,由于其带来的性能或可用性问题,一般的两阶段提交支持太昂贵了。 我们认为应用程序员应该处理由于过度使用而导致的性能问题,而不是在缺乏事务的情况下编写代码
> 一些作者声称,支持通用的两阶段提交代价太大,会带来性能与可用性的问题。让程序员来处理过度使用事务导致的性能问题,总比缺少事务编程好得多
>
> ——James Corbett等人SpannerGoogle的全球分布式数据库2012
@ -10,109 +10,134 @@
[TOC]
在数据系统的残酷现实中,很多事情可能会出错:
在数据系统的严酷现实中,很多事情可能会出错:
- 数据库软件或硬件可能随时发生故障(包括写操作过程中)。
- 应用程序可能随时崩溃(包括一系列操作的中途)。
- 网络中断可能会意外地切断来自数据库的应用程序,或从另一个数据库节点切断应用程序。
- 数据库软件或硬件可能随时发生故障(包括写到一半时)。
- 应用程序可能随时崩溃(包括一系列操作的中间)。
- 网络中断可能会意外地切断来自数据库与应用的联系,或数据库之间的联系。
- 多个客户端可能会同时写入数据库,覆盖彼此的更改。
- 客户可能读取的数据无意义,因为它只是部分更新。
- 客户可能读取到无意义的数据,因为数据只是部分更新。
- 客户之间的竞争条件可能导致令人惊讶的错误。
为了可靠,系统必须处理这些故障并确保它们不会导致整个系统的灾难性故障。但是,实现容错机制是很多工作。需要仔细考虑所有可能出错的事情,并进行大量的测试以确保解决方案真正起作用。
为了可靠,系统必须处理这些故障并确保它们不会导致整个系统的灾难性故障。但是,实现容错机制工作量很大。需要仔细考虑所有可能出错的事情,并进行大量的测试以确保解决方案真正用。
数十年来,交易一直是简化这些问题的首选机制。事务是应用程序将多个读取和写入组合成逻辑单元的一种方式。从概念上讲,事务中的所有读写都是作为一个操作执行的:整个事务成功(提交)或失败(中止,回滚)。如果失败,应用程序可以安全地重试。对于事务来说,应用程序的错误处理变得简单多了,因为它不需要担心部分失败,即某些操作成功,有些失败(无论出于何种原因)的情况。
如果你花了数年时间处理交易,看起来很明显,但我们不应该把它们视为理所当然。交易不是一种自然规律;它们是为了简化访问数据库的应用程序的编程模型而创建的。通过使用事务,应用程序可以自由地忽略某些潜在的错误情况和并发问题,因为数据库会替代它们(我们称之为安全保证)。
并不是所有的应用程序都需要事务处理,有时候有利于削弱事务保证或完全放弃它们(例如,为了获得更高的性能或更高的可用性)。一些安全属性可以在没有交易的情况
你怎么知道你是否需要交易?为了回答这个问题,我们首先需要确切地理解交易可以提供的安全保障,以及与这些交易相关的成本。尽管乍一看交易似乎很简单,但实际上有许多微妙而重要的细节正在发挥作用。
在本章中,我们将研究许多可能出错的事例,并探讨数据库用于防范这些问题的算法。我们将在并发控制领域特别深入地讨论可能发生的各种竞争条件以及数据库如何实现读取提交,快照隔离和可串行化等隔离级别。
本章适用于单节点和分布式数据库;在第8章中我们将重点讨论仅在分布式系统中出现的特殊挑战。
数十年来,**事务transaction**一直是简化这些问题的首选机制。事务是应用程序将多个读取和写入组合成逻辑单元的一种方式。从概念上讲,事务中的所有读写都是作为一个操作执行的:整个事务成功(**提交commit**)或失败(**中止abort****回滚rollback**)。如果失败,应用程序可以安全地重试。对于事务来说,应用程序的错误处理变得简单多了,因为它不需要担心部分失败,即某些操作成功,有些失败(无论出于何种原因)的情况。
和事务打交道时间长了,你可能会觉得它很明显。但我们不应该把它们视为理所当然。事务不是一种自然规律;它们是为了**简化应用编程模型**而创建的。通过使用事务,应用程序可以自由地忽略某些潜在的错误情况和并发问题,因为数据库会替它们处理好这些。(我们称之为**安全保证safety guarantees**)。
并不是所有的应用都需要事务,有时候弱化事务保证或完全放弃它们也是有利的(例如,为了获得更高的性能或更高的可用性)。可以在没有事务的情况下实现一些安全属性。
你怎么知道你是否需要事务?为了回答这个问题,我们首先需要确切地理解事务可以提供的安全保障,以及与这些事务相关的成本。尽管乍一看事务似乎很简单,但实际上有许多微妙而重要的细节在发挥作用。
在本章中,我们将研究许多可能出错的事例,并探讨数据库用于防范这些问题的算法。我们将在并发控制领域特别深入地讨论可能发生的各种竞争条件以及数据库如何实现**读已提交****快照隔离**和**可串行化**等隔离级别。
本章同时适用于单点与分布式数据库在第8章中我们将重点讨论仅在分布式系统中出现的特殊挑战。
## 事务的棘手概念
现在几乎所有的关系数据库和一些非关系数据库都支持事务处理。他们中的大多数遵循IBM系统R第一个SQL数据库在1975年引入的风格[1,23]。尽管一些实现细节已经改变但总体思路在40年中几乎保持不变MySQLPostgreSQLOracleSQL Server等中的事务支持与系统R的支持非常相似。
在二十一世纪末期非关系NoSQL数据库开始普及。他们的目标是通过提供新的数据模型选择参见第2章并通过默认包括复制第5章和分区第6章来改善关系现状。交易是这种运动的主要原因这些新一代数据库中的许多数据库完全放弃了交易或者重新定义了这个词来描述比以前更为理解的更弱的一套保证[4]。
随着这种新型分布式数据库的炒作,人们普遍认为交易是可扩展性的对立面,任何大型系统都必须放弃交易以保持良好的性能和高可用性[5 6]。另一方面,数据库供应商有时将交易保证作为“重要应用”和“有价值数据”的基本要求。这两种观点都是纯粹的夸张。
事实并非如此简单:与其他技术设计选择一样,交易具有优势和局限性。为了理解这些权衡,让我们进入交易可以提供的保证的细节 - 无论是在正常运行中还是在各种极端(但是现实的)情况下。
现在几乎所有的关系数据库和一些非关系数据库都支持事务处理。他们中的大多数遵循IBM系统R第一个SQL数据库在1975年引入的风格[1,23]。尽管一些实现细节已经改变但总体思路在40年中几乎保持不变MySQLPostgreSQLOracleSQL Server等中的事务支持与系统R出乎寻常地相似。
在二十一世纪末期非关系NoSQL数据库开始普及。它们的目标是通过提供新的数据模型选择参见第2章并通过默认包含复制第5章和分区第6章来改善关系现状。事务是这种运动的主要原因这些新一代数据库中的许多数据库完全放弃了事务或者重新定义了这个词描述比以前理解所更弱的一套保证[4]。
随着这种新型分布式数据库的炒作,人们普遍认为事务是可扩展性的对立面,任何大型系统都必须放弃事务以保持良好的性能和高可用性[5 6]。另一方面,数据库厂商有时将事务保证作为“重要应用”和“有价值数据”的基本要求。这两种观点都是**纯粹的夸张**。
事实并非如此简单:与其他技术设计选择一样,事务有其优势和局限性。为了理解这些权衡,让我们了解事务所提供保证的细节——无论是在正常运行中还是在各种极端(但是现实存在)情况下。
### ACID的含义
交易所提供的安全保证通常由众所周知的首字母缩略词ACID来描述ACID代表原子性一致性隔离性和耐久性。它由TheoHärder和Andreas Reuter于1983年创建旨在为数据库中的容错机制建立精确的术语。
但实际上一个数据库的ACID实现并不等于另一个实现。例如我们将会看到围绕着隔离的含义有许多含糊不清[8]。高层的想法是健全的但恶魔是在细节。今天当一个系统声称是“符合ACID”的时候目前还不清楚你可以期待什么保证。不幸的是ACID主要是一个营销术语。
不符合ACID标准的系统有时被称为BASE它代表基本可用性软状态和最终一致性[9]这比ACID的定义更加模糊似乎BASE的唯一合理的定义是“不是ACID”即它几乎可以代表任何你想要的东西。
让我们深入了解原子性,一致性,隔离性和持久性的定义,因为这可以让我们改进我们的交易思想。
事务所提供的安全保证通常由众所周知的首字母缩略词ACID来描述ACID代表**原子性Atomicity****一致性Consistency****隔离性Isolation**和**持久性Durability**。它由TheoHärder和Andreas Reuter于1983年创建旨在为数据库中的容错机制建立精确的术语。
#### Atomicity: 原子性
但实际上不同数据库的ACID实现并不相同。例如我们将会看到围绕着**隔离Isolation**的含义有许多含糊不清[8]。高层次上的想法是合理的但魔鬼隐藏在细节中。今天当一个系统声称自己“符合ACID”时实际上能期待的是什么保证并不清楚。不幸的是ACID现在几乎已经变成了一个营销术语。
不符合ACID标准的系统有时被称为BASE它代表**基本可用性Basically Available****软状态Soft State**和**最终一致性Eventual consistency**[9]这比ACID的定义更加模糊似乎BASE的唯一合理的定义是“不是ACID”即它几乎可以代表任何你想要的东西。
让我们深入了解原子性,一致性,隔离性和持久性的定义,这可以让我们提炼出事务的思想。
#### 原子性Atomicity
一般来说,原子是指不能分解成小部分的东西。这个词在计算的不同分支中意味着相似但又微妙不同的东西。例如,在多线程编程中,如果一个线程执行一个原子操作,这意味着另一个线程无法看到该操作的一半结果。系统只能处于操作之前或操作之后的状态,而不是介于两者之间的状态。
相比之下在ACID的情况下原子性不是关于并发性。它没有描述如果几个进程试图同时访问相同的数据会发生什么情况因为它包含在字母I下用于隔离请参见“隔离”第195页
而是ACID原子性描述了如果客户想要进行多次写入会发生什么情况但是在处理了一些写入之后发生故障例如进程崩溃网络连接中断磁盘变满或者某种完整性约束被违反。如果这些写入被分组到一个原子事务中并且该事务由于错误而不能完成提交则该事务将被中止并且数据库必须丢弃或撤消该事务中迄今为止所做的任何写入。
没有原子性,如果通过多次更改发生错误,很难知道哪些更改已经生效,哪些没有生效。该应用程序可以再试一次,但冒着两次相同的风险,导致重复或不正确的数据。原子性简化了这个问题:如果事务被中止,应用程序可以确定它没有改变任何东西,所以它可以安全地重试。
ACID原子性的定义特征是能够在错误中止事务并且丢弃来自该事务的所有写入的能力。或许堕胎将是一个比原子性更好的术语但是我们将坚持原子性因为这是通常的词。
#### Consistency一致性
相比之下ACID的原子性并**不**是关于**并发concurrent**的。它并不是在描述如果几个进程试图同时访问相同的数据会发生什么情况这种情况包含在I中即隔离Isolation请参见“隔离”第195页
ACID的原子性描述了当客户想进行多次写入但在一些写入处理完之后出现故障的情况。例如进程崩溃网络连接中断磁盘变满或者某种完整性约束被违反。如果这些写入被分组到一个原子事务中并且该事务由于错误而不能完成提交则该事务将被中止并且数据库必须丢弃或撤消该事务中迄今为止所做的任何写入。
如果没有原子性,在多处更改进行到一半时发生错误,很难知道哪些更改已经生效,哪些没有生效。该应用程序可以再试一次,但冒着进行两次相同变更的风险,可能会导致数据重复或错误的数据。原子性简化了这个问题:如果事务被**中止abort**,应用程序可以确定它没有改变任何东西,所以可以安全地重试。
ACID原子性的定义特征是能够在错误中止事务并丢弃该事务所有变更的能力。或许**可中止性abortability**将是一个比原子性更好的术语,但是我们将坚持使用原子性,因为这是通常使用的词。
#### 一致性Consistency
一致性这个词重载的很厉害:
* 在第5章中我们讨论了副本一致性以及在异步复制系统中出现的最终一致性问题请参阅第161页上的“复制滞后问题”
* 一致性哈希是某些系统用于重新分区的一种分区方法请参阅“一致性散列”第191页
* 在CAP定理参见第9章一致性一词用于表示可线性化请参见“线性化”第295页
* 在ACID的情况下**一致性**是指数据库在应用程序的特定概念中处于“良好状态”。
一致性这个词非常重要:
•在第5章中我们讨论了副本一致性以及在异步复制系统中出现的最终一致性问题请参阅第161页上的“复制滞后问题”
•一致性散列是某些系统用于重新分区的一种分区方法请参阅“一致性散列”第191页
•在CAP定理参见第9章一致性一词用于表示可线性化请参见“线性化”第295页
•在ACID的情况下一致性是指数据库的应用程序特定概念处于“良好状态”。
不幸的是,同一个词至少有四种不同的含义。
ACID一致性的概念是您对数据不变量有一定的陈述这些陈述必须始终是真实的例如在会计系统中所有账户的信用和借记必须始终保持平衡。如果一个事务以一个根据这些不变量有效的数据库开始并且在事务处理期间的任何写入保持有效性那么你可以确定不变量总是被满足。
但是,这种一致性的思想取决于应用程序的不变量的概念,应用程序有责任正确定义它的事务,以保持一致性。这不是数据库可以保证的事情:如果您编写的数据违反了您的不变量,数据库无法阻止您。 (一些特定类型的不变量可以由数据库检查,例如使用外键约束或唯一性约束,但是一般来说,应用程序定义什么数据是有效的或者无效的 - 数据库只存储它。
原子性隔离性和持久性是数据库的属性而一致性在ACID意义上是应用程序的属性。应用程序可能依赖于数据库的原子性和隔离属性来实现一致性但这并不取决于数据库本身。因此字母C不属于ACID.i
#### Isolation隔离
ACID一致性的概念是对数据的一组始终为真的特定陈述。即**不变量invariants**。例如,在会计系统中,所有账户的信用和债务必须始终保持平衡。如果一个事务从满足这些不变量的有效数据库开始,且在事务处理期间的任何写入都保持这种有效性,那么可以确定,不变量总是满足的。
大多数数据库同时被多个客户端访问。如果他们读取和写入数据库的不同部分,这是没有问题的,但是如果他们正在访问相同的数据库记录,则可能会遇到并发问题(竞争条件)。
图7-1是这类问题的一个简单例子。假设你有两个客户同时增加一个存储在数据库中的计数器。每个客户端需要读取当前值加1并写回新值假设数据库中没有增加操作。在图7-1中柜台应该从42增加到44因为两个增量发生了但实际上由于竞态条件只能到43。
ACID意义上的隔离意味着同时执行的事务是相互隔离的它们不能彼此的脚趾。传统的数据库教科书将隔离形式化为可序列化这意味着每个事务可以假装它是在整个数据库上运行的唯一事务。数据库确保当事务已经提交时结果与它们连续运行一个接一个是一样的尽管实际上它们可能已经运行了[10]。
但是,这种一致性的思想取决于应用程序的不变量的概念,应用程序有责任正确定义它的事务,以保持一致性。这不是数据库可以保证的事情:如果你写入违反不变量的脏数据,数据库无法阻止你。 (一些特定类型的不变量可以由数据库检查,例如使用外键约束或唯一性约束,但是一般来说,应用程序定义什么数据是有效的或者无效的—— 数据库只存储它。)
原子性隔离性和持久性是数据库的属性而一致性在ACID意义上是应用程序的属性。应用程序可能依赖于数据库的原子性和隔离属性来实现一致性但这并不取决于数据库本身。因此字母C不属于ACID[^i]。
[^i]: 乔·海勒斯坦Joe Hellerstein指出在海德尔和路透社的论文中“ACID中的C”是被“扔进去凑缩写单词的”而且那时候C并没有被认为很重要。
#### 隔离Isolation
大多数数据库会同时被多个客户端访问。如果它们各自读写数据库的不同部分,这是没有问题的,但是如果它们正在访问相同的数据库记录,则可能会遇到**并发**问题(**竞争条件race conditions**)。
图7-1是这类问题的一个简单例子。假设你有两个客户同时增加一个存储在数据库中的计数器。每个客户端需要读取当前值加1再回写新值假设数据库中没有自增操作。在图7-1中计数器应该从42增至44因为发生了两次自增但由于竞态条件实际上只到了43。
ACID意义上的隔离意味着同时执行的事务是相互隔离的它们不能踩到彼此的脚。传统的数据库教科书将隔离形式化为**可序列化Serializability**,这意味着每个事务可以假装它是在整个数据库上运行的唯一事务。数据库确保当事务已经提交时,结果与它们按顺序运行(一个接一个)是一样的,尽管实际上它们可能是并发运行的[10]。
![](img/fig7-1.png)
**图7-1 两个客户之间的竞争状态同时递增计数器。**
然而在实践中很少使用可序列化隔离因为它带来了性能损失。一些流行的数据库如Oracle 11g甚至没有实现它。在Oracle中有一个名为“serializable”的隔离级别但实际上它实现了一种叫做快照隔离的功能这是一种比serializability更弱的保证[8,11]。我们将在第233页的“弱等级”中探索快照隔离和其他形式的隔离。
然而在实践中,很少使用可序列化隔离,因为它带来性能损失。一些流行的数据库如Oracle 11g甚至没有实现它。在Oracle中有一个名为“serializable”的隔离级别但实际上它实现了一种叫做**快照隔离**的功能,这是一种比可序列化更弱的保证[8,11]。我们将在第233页的“[隔离等级]()”中探索快照隔离和其他形式的隔离。
#### Durability 持久性
#### 持久性Durability
数据库系统的目的是提供一个安全的地方,可以存储数据而不用担心丢失数据。耐久性是一个承诺,即一旦交易成功完成,即使存在硬件故障或数据库崩溃,所写的任何数据也不会被遗忘。
在单节点数据库中耐久性通常意味着数据已被写入非易失性存储设备如硬盘驱动器或SSD。它通常还包括预写日志或类似的文件请参阅第77页的“使B树可靠”以便在磁盘上的数据结构损坏时进行恢复。在复制的数据库中可用性可能意味着数据已成功复制到某些节点。为了提供持久性保证数据库必须等到这些写入或复制完成后才能报告事务成功提交。
如第6页的“可靠性”中所述完美的持久性不存在如果所有硬盘和所有备份同时被销毁那么显然没有任何数据库能够为您节省。
数据库系统的目的是提供一个安全的地方可以存储数据而不用担心丢失数据。Durability是一个承诺即一旦事务成功完成即使存在硬件故障或数据库崩溃所写的任何数据也不会被遗忘。
在单节点数据库中持久性通常意味着数据已被写入非易失性存储设备如硬盘驱动器或SSD。它通常还包括预写日志或类似的文件请参阅第77页的“[使B树可靠]()”),以便在磁盘上的数据结构损坏时进行恢复。在有复制的数据库中,可用性可能意味着数据已成功复制到一些节点。为了提供持久性保证,数据库必须等到这些写入或复制完成后才能报告事务成功提交。
如第6页的“可靠性”中所述完美的持久性不存在如果所有硬盘和所有备份同时被销毁那么显然没有任何数据库能拯救你了。
> #### 复制和持久性
>
> 从历史上看耐用性意味着写入存档磁带。然后它被理解为写入磁盘或SSD。最近它已被改编为意味着复制。哪个实施更好
> 事实是,没有什么是完美的:
> •如果您写入磁盘并且机器死机,即使您的数据没有丢失,在您修复机器或将磁盘传输到另一台机器之前,也无法访问。复制的系统可以保持可用。
> •一个相关的故障停电或一个可能导致特定输入的每个节点崩溃的错误可能会一次性删除所有副本请参阅第6页的「可靠性」丢失任何仅在内存中的数据。因此写入磁盘仍然与内存数据库相关。
> •在异步复制系统中当引导器变得不可用时最近的写入操作可能会丢失请参阅第156页的「处理节点中断」
> •当电源突然断电时特别是固态硬盘被证明有时违反了应有的保证甚至fsync也不能保证正常工作[12]。磁盘固件可能有错误,就像任何其他类型的软件一样[13,14]。
> 存储引擎和文件系统之间的微妙交互可能会导致难以追踪的错误,并可能导致磁盘上的文件在崩溃后被损坏[15,16]。
> •磁盘上的数据可能会逐渐被破坏而不会被检测到[17]。如果数据已损坏一段时间,副本和最近的备份也可能损坏。在这种情况下,您将需要尝试从历史备份中恢复数据。
> •一项关于固态硬盘的研究发现在运行的前四年30到80的硬盘至少发生一个坏块[18]。磁性硬盘驱动器的坏道率较低但比SSD更高的完全故障率。
> •如果SSD断电可能会在几周内开始丢失数据具体取决于温度[19]。
> 在实践中,没有一种技术可以提供绝对的保证。只有各种降低风险的技术,包括写入磁盘,复制到远程机器和备份 - 它们可以并且应该一起使用。与往常一样,采取任何理论上的“保证”,用一粒健康的盐都是明智的做法。
> 在历史上持久性意味着写入归档磁带。然后它被理解为写入磁盘或SSD。最近它已被改编为意味着复制。
>
> 哪种实现更好?
>
> 真相是,没有什么是完美的:
>
> * 如果您写入磁盘并且机器死机,即使您的数据没有丢失,在您修复机器或将磁盘传输到另一台机器之前,也无法访问。复制的系统可以保持可用。
> * 一个相关的故障停电或一个在特定输入时导致所有节点崩溃的Bug可能会一次性删除所有副本请参阅第6页的「可靠性」丢失任何仅存在内存中的数据。因此内存数据库仍然与磁盘写入相关。
> * 在异步复制系统中当主库不可用时最近的写入操作可能会丢失请参阅第156页的「处理节点中断」
> * 当电源突然断电时特别是固态硬盘被证明有时会违反应有的保证甚至fsync也不能保证正常工作[12]。磁盘固件可能有错误,就像任何其他类型的软件一样[13,14]。
> * 存储引擎和文件系统之间的微妙交互可能会导致难以追踪的错误,并可能导致磁盘上的文件在崩溃后被损坏[15,16]。
> * 磁盘上的数据可能会逐渐被破坏而不会被检测到[17]。如果数据已损坏一段时间,副本和最近的备份也可能损坏。在这种情况下,您将需要尝试从历史备份中恢复数据。
> * 一项关于固态硬盘的研究发现在运行的前四年30到80的硬盘至少发生一个坏块[18]。磁性硬盘驱动器的坏道率较低但比SSD更高的完全故障率。
> * 如果SSD断电可能会在几周内开始丢失数据具体取决于温度[19]。
>
> 在实践中,没有一种技术可以提供绝对的保证。只有各种降低风险的技术,包括写入磁盘,复制到远程机器和备份——它们可以并且应该一起使用。与往常一样,最好抱着怀疑的态度采纳任何理论上的“保证”
### 单对象和多对象操作
回顾一下在ACID中原子性和隔离性描述了如果客户在同一事务中进行多次写入时数据库应该做的事情
- 原子性
***原子性***
如果在一系列写操作的中途发生错误,则应中止事务处理,并废除写入该处的写操作。换句话说,数据库不必担心部分失败,通过提供全或无的保证。
如果在一系列写操作的中途发生错误,则应中止事务处理,并丢弃当前事务的所有写入。换句话说,数据库免去了用户对部分失败的担忧——通过提供“要么不做,要做全做”的保证。
- 隔离
***隔离***
同时运行的交易不应该互相干扰。例如,如果一个事务进行多次写入,则另一个事务应该看到全部或者全部写入,而不是一些子集。
同时运行的事务不应该互相干扰。例如,如果一个事务进行多次写入,则另一个事务应该看到全部或者全部写入,而不是一些子集。
这些定义假定您想一次修改几个对象文档记录。如果需要保持多个数据同步则通常需要这种多对象事务。图7-2显示了一个来自电子邮件应用程序的例子。要显示用户的未读邮件数量可以查询如下所示的内容
@ -124,7 +149,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
在图7-2中用户2遇到异常情况邮箱列表显示未读消息但计数器显示零未读消息因为计数器增量还没有发生.ii隔离将通过确保用户2看到插入的电子邮件和更新的计数器或者都不是但不是一个不一致的中间点。
[^ii]: 可以说,电子邮件应用程序中的错误计数器并不是特别重要的问题。 或者,考虑一个客户账户余额,而不是一个未读的柜台,而不是一个电子邮件的支付交易
[^ii]: 可以说,电子邮件应用程序中的错误计数器并不是特别重要的问题。 或者,考虑一个客户的账户余额,而不是未读的计数器,一次支付事务而不是电子邮件
![](img/fig7-2.png)
@ -134,11 +159,11 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
![](img/fig7-3.png)
**图7-3原子性可以确保,如果发生错误,则该事务的任何先前写入都会被撤消,以避免不一致的状态。**
**图7-3 原子性可以确保,如果发生错误,则该事务的任何先前写入都会被撤消,以避免不一致的状态。**
多对象事务需要某种方式来确定哪些读写操作属于同一个事务。在关系数据库中通常基于客户端到数据库服务器的TCP连接来完成在任何特定连接上`BEGIN TRANSACTION`和`COMMIT`语句之间的所有内容都被认为是同一事务的一部分.[^iii]
[^iii]: 这并不理想。如果TCP连接中断则事务必须中止。如果中断发生在客户端请求提交之后但在服务器确认提交发生之前客户端不知道交易是否已提交。为了解决这个问题事务管理器可以通过一个唯一的事务标识符对操作进行分组这个标识符没有绑定到特定的TCP连接。我们将回到第516页的“数据库的端到端的争论”中的这个主题。
[^iii]: 这并不理想。如果TCP连接中断则事务必须中止。如果中断发生在客户端请求提交之后但在服务器确认提交发生之前客户端不知道事务是否已提交。为了解决这个问题事务管理器可以通过一个唯一的事务标识符对操作进行分组这个标识符没有绑定到特定的TCP连接。我们将回到第516页的“数据库的端到端的争论”中的这个主题。
另一方面许多非关系数据库并没有将这些操作组合在一起的方法。即使存在多对象API例如键值存储可能具有在一个操作中更新几个键的多重放置操作但这并不一定意味着它具有事务语义该命令可能成功一些键和其他的失败使数据库处于部分更新的状态。
@ -150,27 +175,31 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
- 如果在数据库正在覆盖磁盘上的前一个值的过程中电源发生故障,是否最终将新旧值拼接在一起?
- 如果另一个客户端在写入过程中读取该文档,是否会看到部分更新的值?
这些问题会令人难以置信的混淆,因此存储引擎几乎普遍的目的是在一个节点上的单个对象例如键值对上提供原子性和隔离性。原子性可以通过使用日志来实现崩溃恢复请参阅第82页的“使B树可靠”并且可以使用每个对象的锁来实现隔离每次只允许一个线程访问对象 )。
这些问题会令人难以置信的困惑,因此存储引擎几乎普遍的目标是在一个节点上的单个对象例如键值对上提供原子性和隔离性。原子性可以通过使用日志来实现崩溃恢复请参阅第82页的“使B树可靠”并且可以使用每个对象的锁来实现隔离每次只允许一个线程访问对象 )。
一些数据库也提供更复杂的原子操作例如增量操作这样就不需要像图7-1那样的读 - 修改 - 写循环。同样流行的是比较和设置操作只有当其他人没有同时更改该值时才允许进行写操作请参见“比较和设置”一节第245页
一些数据库也提供更复杂的原子操作例如增量操作这样就不需要像图7-1那样的读 - 修改 - 写循环。同样流行的是**比较和设置CAS, compare-and-set**操作只有当其他人没有同时更改该值时才允许进行写操作请参见“比较和设置”一节第245页
这些单对象操作很有用因为它们可以防止在多个客户端尝试同时写入同一个对象时丢失更新请参阅“防止丢失的更新”第221页。但是它们不是通常意义上的交易。比较和设置以及其他单一对象操作被称为“轻量级事务”甚至被称为“ACID”[20,21,22],但是这个术语是误导性的。事务通常被理解为将多个对象上的多个操作分组为一个执行单元的机制。[^iv]
这些单对象操作很有用因为它们可以防止在多个客户端尝试同时写入同一个对象时丢失更新请参阅“防止丢失的更新”第221页。但是它们不是通常意义上的事务。CAS以及其他单一对象操作被称为“轻量级事务”,甚至出于营销目的被称为“ACID”[20,21,22],但是这个术语是误导性的。事务通常被理解为将多个对象上的多个操作分组为一个执行单元的机制。[^iv]
[^iv]: 严格地说,原子增量这个术语在多线程编程的意义上使用了原子这个词。 在ACID的情况下它实际上应该被称为孤立的或可序列化的增量。 但是这是越来越nitpicky
[^iv]: 严格地说,**原子自增atomic increment**这个术语在多线程编程的意义上使用了原子这个词。 在ACID的情况下它实际上应该被称为**孤立isolated**的或**可序列化serializable**的增量。 但这就太吹毛求疵了
#### 需要多对象事务
#### 多对象事务的需求
许多分布式数据存储已经放弃了多对象事务因为它们很难跨分区实现而且在需要高可用性或高性能的情况下它们可能会遇到阻碍。但是没有什么能从根本上防止分布式数据库中的事务我们将在第9章讨论分布式事务的实现。
但是我们是否需要多对象交易?是否有可能只用键值数据模型和单对象操作来实现任何应用程序?
有一些使用情况下,单对象插入,更新和删除是足够的。但是,在许多其他情况下,需要协调写入几个不同的对象:
•在关系数据模型中,一个表中的行通常具有对另一个表中的行的外键引用。 (同样,在一个类似图形的数据模型中,一个顶点与其他顶点有边)。多对象事务允许你确保这些引用保持有效:当插入几个相互引用的记录时,外键有是正确的和最新的,或者数据变得荒谬。
•在文档数据模型中,需要一起更新的字段通常在同一个文档中,这被视为单个对象 - 更新单个文档时不需要多对象事务。但是缺乏连接功能的文档数据库也会鼓励非规范化请参阅第38页上的“与文档数据库相关的对比”。当需要更新非规范化的信息时如图7-2所示您需要一次更新多个文档。事务在这种情况下非常有用可以防止非规范化的数据不同步。
•在具有二级索引的数据库中(几乎除了纯粹的键值存储以外的所有内容),每次更改值时都需要更新索引。从事务角度来看,这些索引是不同的数据库对象:例如,如果没有事务隔离,记录可能出现在一个索引中,而不是另一个索引中,因为第二个索引的更新还没有发生。
这些应用程序仍然可以在没有交易的情况然而没有原子性错误处理就变得复杂得多缺乏隔离性会导致并发问题。我们将在第233页的“弱隔离级别”中讨论这些问题并在第12章中探讨其他方法。
许多分布式数据存储已经放弃了多对象事务因为它们很难跨分区实现而且在需要高可用性或高性能的情况下它们可能会碍事挡路。但就算是分布式系统也并没有什么根本上能拒绝事务的原因我们将在第9章讨论分布式事务的实现。
但是我们是否需要多对象事务?是否有可能只用键值数据模型和单对象操作来实现任何应用程序?
有一些场景中,单对象插入,更新和删除是足够的。但是许多其他场景需要协调写入几个不同的对象:
* 在关系数据模型中,一个表中的行通常具有对另一个表中的行的外键引用。 (同样,在一个类似图形的数据模型中,一个顶点与其他顶点有边)。多对象事务允许你确保这些引用保持有效:当插入几个相互引用的记录时,外键有是正确的和最新的,或者数据变得荒谬。
* 在文档数据模型中,需要一起更新的字段通常在同一个文档中,这被视为单个对象 - 更新单个文档时不需要多对象事务。但是缺乏连接功能的文档数据库也会鼓励非规范化请参阅第38页上的“与文档数据库相关的对比”。当需要更新非规范化的信息时如图7-2所示您需要一次更新多个文档。事务在这种情况下非常有用可以防止非规范化的数据不同步。
* 在具有二级索引的数据库中(几乎除了纯粹的键值存储以外的所有内容),每次更改值时都需要更新索引。从事务角度来看,这些索引是不同的数据库对象:例如,如果没有事务隔离,记录可能出现在一个索引中,而不是另一个索引中,因为第二个索引的更新还没有发生。
这些应用程序仍然可以在没有事务的情况然而没有原子性错误处理就变得复杂得多缺乏隔离性会导致并发问题。我们将在第233页的“弱隔离级别”中讨论这些问题并在第12章中探讨其他方法。
#### 处理错误和中止
事务的一个关键特性是,如果发生错误,它可以被中止并安全地重试。 ACID数据库是基于这样一种理念如果数据库有违反其原子性隔离性或持久性的危险则完全放弃交易而不是完全放弃交易。
事务的一个关键特性是,如果发生错误,它可以被中止并安全地重试。 ACID数据库是基于这样一种理念如果数据库有违反其原子性隔离性或持久性的危险则完全放弃事务,而不是完全放弃事务
不是所有的系统都遵循这个理念。具体而言具有无引导复制的数据存储请参见第167页的“无引导复制”在“尽力而为”的基础上工作得更多可以概括为“数据库将尽其所能并且运行到一个错误它不会撤消它已经完成的事情“ - 所以这是应用程序的责任,从错误中恢复。
@ -179,9 +208,9 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
虽然重试一个中止的事务是一个简单而有效的错误处理机制,但它并不完美:
- 如果事务实际上成功了,但是在服务器试图确认成功提交给客户端(所以客户端认为失败)时网络发生故障,那么重试事务会导致它被执行两次,除非你有一个额外的应用程序,级别的重复数据删除机制已到位。
- 如果错误是由于过载造成的,则重试交易将使问题变得更糟,而不是更好。为了避免这种反馈周期,您可以限制重试次数,使用指数回退,并处理与过载相关的错误(与可能的情况不同)。
- 如果错误是由于过载造成的,则重试事务将使问题变得更糟,而不是更好。为了避免这种反馈周期,您可以限制重试次数,使用指数回退,并处理与过载相关的错误(与可能的情况不同)。
- 仅在暂时性错误(例如,由于死锁,异常情况,临时性网络中断和故障转移)之后才值得重试。在发生永久性错误(例如,违反约束)之后,重试将毫无意义。
- 如果事务在数据库之外也有副作用,即使事务被中止,也可能发生这些副作用。例如,如果您正在发送电子邮件,则每次重试交易时都不会再发送电子邮件。如果您想确保几个不同的系统提交或放弃在一起两阶段提交可以提供帮助我们将在第354页的“原子提交和两阶段提交2PC”中讨论这个问题
- 如果事务在数据库之外也有副作用,即使事务被中止,也可能发生这些副作用。例如,如果您正在发送电子邮件,则每次重试事务时都不会再发送电子邮件。如果您想确保几个不同的系统提交或放弃在一起两阶段提交可以提供帮助我们将在第354页的“原子提交和两阶段提交2PC”中讨论这个问题
- 如果客户端进程在重试时失败,则任何试图写入数据库的数据都将丢失。
## 弱隔离级别
@ -245,17 +274,17 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
读提交是一个非常流行的隔离级别。这是Oracle 11gPostgreSQLSQL Server 2012MemSQL和其他许多数据库的默认设置[8]。
最常见的情况是,数据库通过使用行级锁来防止脏写入:当事务想要修改特定对象(行或文档)时,它必须首先获得该对象的锁。然后必须保持该锁,直到交易被提交或中止。只有一个事务可以保存任何给定对象的锁;如果另一个事务要写入同一个对象,则必须等到第一个事务被提交或中止后才能获取该锁并继续。这种锁定是通过读取提交模式(或更强的隔离级别)中的数据库自动完成的。
最常见的情况是,数据库通过使用行级锁来防止脏写入:当事务想要修改特定对象(行或文档)时,它必须首先获得该对象的锁。然后必须保持该锁,直到事务被提交或中止。只有一个事务可以保存任何给定对象的锁;如果另一个事务要写入同一个对象,则必须等到第一个事务被提交或中止后才能获取该锁并继续。这种锁定是通过读取提交模式(或更强的隔离级别)中的数据库自动完成的。
我们如何防止脏读?一种选择是使用相同的锁,并要求任何想要读取对象的事务来简单地获取该锁,然后在读取之后立即再次释放该锁。这将确保读取不会发生,而对象有一个肮脏的,未提交的值(因为在那段时间锁将由举行了写的事务)。
但是,要求读取锁定的方法在实际中并不奏效,因为长时间运行的写入事务会强制许多只读事务等待长时间运行的事务完成。这会损害只读事务的响应时间,并且不利于可操作性:由于等待锁定,应用程序的某个部分的减速可能会在完全不同的应用程序中产生连锁效应。
出于这个原因,大多数数据库[^vi]使用上述方法防止脏读。如图7-4对于写入的每个对象数据库都会记住旧的提交值和由当前持有写入锁的事务设置的新值。 当交易正在进行时,任何其他读取对象的交易都被赋予旧的价值。 只有当提交新值时,交易才会切换到读取新值。
出于这个原因,大多数数据库[^vi]使用上述方法防止脏读。如图7-4对于写入的每个对象数据库都会记住旧的提交值和由当前持有写入锁的事务设置的新值。 当事务正在进行时,任何其他读取对象的事务都被赋予旧的价值。 只有当提交新值时,事务才会切换到读取新值。
### 快照隔离和可重复读取
如果你从表面上看读取承诺的隔离,你可以原谅它认为事务需要做的一切事情:它允许中止(原子性要求),它防止读取不完整的事务结果,并排写入的并发写入。事实上,这些是非常有用的功能,而且比没有交易的系统可以得到更多的保证。
如果你从表面上看读取承诺的隔离,你可以原谅它认为事务需要做的一切事情:它允许中止(原子性要求),它防止读取不完整的事务结果,并排写入的并发写入。事实上,这些是非常有用的功能,而且比没有事务的系统可以得到更多的保证。
但是在使用此隔离级别时仍然有很多方法可能会导致并发错误。例如图7-6说明了提交读取时可能发生的问题。
@ -263,11 +292,11 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
**图7-6 读取偏斜Alice观察数据库处于不一致的状态**
说爱丽丝在银行有1000美元的储蓄分两个账户每个500美元。现在一笔交易从她的账户中转移了100美元到另一笔账户。如果她不幸在交易正在处理的同一时间查看其账户余额列表则可以在收到付款之前的一段时间看到一个账户余额余额为500美元另一个外汇转账完成后的账户新余额为400美元。对于爱丽丝来说现在她的账户似乎只有900美元 - 看起来100美元已经消失了。
说爱丽丝在银行有1000美元的储蓄分两个账户每个500美元。现在一笔事务从她的账户中转移了100美元到另一笔账户。如果她不幸在事务正在处理的同一时间查看其账户余额列表则可以在收到付款之前的一段时间看到一个账户余额余额为500美元另一个外汇转账完成后的账户新余额为400美元。对于爱丽丝来说现在她的账户似乎只有900美元 - 看起来100美元已经消失了。
[^vi]: 在撰写本文时,唯一使用读取提交隔离锁定的主流数据库是`read_committed_snapshot = off`配置中的IBM DB2和Microsoft SQL Server [23,36]。
这种异常被称为不可重复读取或读取歪斜如果Alice在交易结束时再次读取账户1的余额她将看到与她之前的查询中看到的不同的值600美元。在阅读承诺的隔离条件下阅读偏差被认为是可接受的Alice看到的帐户余额确实是在阅读时确定的。
这种异常被称为不可重复读取或读取歪斜如果Alice在事务结束时再次读取账户1的余额她将看到与她之前的查询中看到的不同的值600美元。在阅读承诺的隔离条件下阅读偏差被认为是可接受的Alice看到的帐户余额确实是在阅读时确定的。
> 不幸的是skew 这个词倾斜是超负荷的我们以前使用它是因为热点的不平衡工作量请参阅第205页上的“偏斜的工作负荷和减轻热点”而这意味着计时异常。
@ -293,7 +322,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
与读取提交的隔离类似快照隔离的实现通常使用写入锁来防止脏写入请参阅“实施读取已提交”第217页这意味着进行写入的事务可以阻止另一个写入同一事务的进程目的。但是读取不需要任何锁定。从性能的角度来看快照隔离的一个关键原则是读者不会阻止作者作者也不会阻止读者。这允许数据库在处理正常写入的同时处理一致快照上的长时间运行的读取查询而两者之间没有任何锁定争用。
为了实现快照隔离数据库使用了我们看到的用于防止图7-4中的脏读的机制的一般化。数据库必须可能保留一个对象的几个不同的提交版本因为各种正在进行的交易可能需要在不同的时间点看到数据库的状态。因为它并排维护着多个版本的对象所以这种技术被称为多版本并发控制MVCC
为了实现快照隔离数据库使用了我们看到的用于防止图7-4中的脏读的机制的一般化。数据库必须可能保留一个对象的几个不同的提交版本因为各种正在进行的事务可能需要在不同的时间点看到数据库的状态。因为它并排维护着多个版本的对象所以这种技术被称为多版本并发控制MVCC
如果一个数据库只需要提供读提交的隔离而不提供快照的隔离那么保留一个对象的两个版本就足够了提交的版本和被覆盖但尚未提交的版本。但是支持快照隔离的存储引擎通常也使用MVCC作为读提交隔离级别。一种典型的方法是提交读取为每个查询使用单独的快照而快照隔离对整个事务使用相同的快照。
@ -307,7 +336,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
表中的每一行都有一个created_by字段其中包含将该行插入到表中的事务的ID。此外每行都有一个deleted_by字段最初是空的。如果某个事务删除了一行那么该行实际上并未从数据库中删除而是通过将deleted_by字段设置为请求删除的事务的ID来标记为删除。在稍后的时间当确定没有事务可以再访问已删除的数据时数据库中的垃圾收集过程将删除标记为删除的行并释放它们的空间。
更新内部翻译成删除和创建。例如在图7-7中交易13从账户2中扣除100美元将余额从500美元改为400美元。账户表现在实际上包含账户2的两行一笔余额为\$500的行被标记为被事务13删除一行的余额为\$400由事务13创建。
更新内部翻译成删除和创建。例如在图7-7中事务13从账户2中扣除100美元将余额从500美元改为400美元。账户表现在实际上包含账户2的两行一笔余额为\$500的行被标记为被事务13删除一行的余额为\$400由事务13创建。
#### 可见性规则用于观察一致的快照
@ -315,12 +344,12 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
数据库可以向应用程序呈现一致的数据库快照。这工作如下:
1. 在每次交易开始时,数据库列出当时所有其他交易(尚未提交或中止)的交易清单。即使交易随后提交,任何写入的交易都会被忽略。
1. 在每次事务开始时,数据库列出当时所有其他事务(尚未提交或中止)的事务清单。即使事务随后提交,任何写入的事务都会被忽略。
2. 被中止的事务所做的任何写入都被忽略。
3. 由具有较晚的事务ID在当前事务开始之后开始的的事务所做的任何写入都被忽略而不管这些事务是否已经提交。
4. 所有其他写入对应用程序的查询都是可见的。
这些规则适用于创建和删除对象。在图7-7中交易12从账户2读取时它看到$ 500的余额因为$ 500余额的删除是由交易13完成的根据规则3交易12看不到交易13所做的删除并且400美元的余额的创建还不可见按照相同的规则
这些规则适用于创建和删除对象。在图7-7中事务12从账户2读取时它看到$ 500的余额因为$ 500余额的删除是由事务13完成的根据规则3事务12看不到事务13所做的删除并且400美元的余额的创建还不可见按照相同的规则
换句话说,如果以下两个条件都成立,则可见一个对象:
@ -352,7 +381,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
到目前为止,我们讨论的读取提交和快照隔离级别主要是保证只读事务在并发写入时可以看到什么。我们主要忽略了同时写入两个事务的问题 - 我们只讨论了脏写入请参阅第235页的“无肮脏的写入”可能会发生一种特定类型的写入 - 写入冲突。
并发写作交易之间还有其他几种有趣的冲突。其中最着名的是丢失更新问题如图7-1所示以两个并发计数器增量为例。
并发写作事务之间还有其他几种有趣的冲突。其中最着名的是丢失更新问题如图7-1所示以两个并发计数器增量为例。
如果应用程序从数据库中读取一些值,修改它并写回修改的值(读取 - 修改 - 写入周期),则可能会发生丢失的更新问题。如果两个事务同时执行,则其中一个修改可能会丢失,因为第二个写入不包括第一个修改。 (我们有时会说后面写的是先前写的。)这种模式发生在各种不同的情况下:
@ -452,13 +481,13 @@ WHERE id = 1234 AND content = 'old content';
**图7-8 写入歪斜导致应用程序错误的示例**
在每次交易中,您的申请首先检查两个或两个以上的医生是否正在通话;如果是的话它假定一名医生可以安全地接通电话。由于数据库使用快照隔离两个检查都返回2所以两个事务都进入下一个阶段。爱丽丝更新自己的记录让自己脱离呼叫而鲍勃也更新自己的记录。这两个交易承诺,现在没有医生在接电话。您的电话至少有一名医生的要求已被违反。
在每次事务中,您的申请首先检查两个或两个以上的医生是否正在通话;如果是的话它假定一名医生可以安全地接通电话。由于数据库使用快照隔离两个检查都返回2所以两个事务都进入下一个阶段。爱丽丝更新自己的记录让自己脱离呼叫而鲍勃也更新自己的记录。这两个事务承诺,现在没有医生在接电话。您的电话至少有一名医生的要求已被违反。
#### 表征写入歪斜
这种异常称为写歪斜[28]。它既不是一个肮脏的写作也不是一个丢失的更新因为这两个事务正在更新两个不同的对象分别是Alice和Bob的待命记录。在这里发生冲突并不那么明显但是这显然是一个竞争条件如果两个交易一个接一个地运行,那么第二个
这种异常称为写歪斜[28]。它既不是一个肮脏的写作也不是一个丢失的更新因为这两个事务正在更新两个不同的对象分别是Alice和Bob的待命记录。在这里发生冲突并不那么明显但是这显然是一个竞争条件如果两个事务一个接一个地运行,那么第二个
医生会被阻止接听电话。异常行为只有在交易同时进行时才有可能。
医生会被阻止接听电话。异常行为只有在事务同时进行时才有可能。
您可以将写入歪斜视为丢失更新问题的一般化。如果两个事务读取相同的对象,然后更新其中一些对象(不同的事务可能更新不同的对象),则可能发生写入歪斜。在不同的事务更新相同的对象的特殊情况下,你会得到一个脏写或丢失更新异常(取决于时间)。
我们看到,有各种不同的方法来防止丢失的更新。随着写歪斜,我们的选择更受限制:
•由于涉及多个对象,原子单对象操作不起作用。
@ -513,11 +542,11 @@ COMMIT;
***声称一个用户名***
在每个用户拥有唯一用户名的网站上,两个用户可能会尝试同时创建具有相同用户名的帐户。您可以使用交易来检查名称是否被采用,如果没有,则使用该名称创建账户。但是,像在前面的例子中那样,在快照隔离下是不安全的。幸运的是,一个唯一的约束是一个简单的解决方案(第二个事务,试图注册用户名将被中止,由于违反约束)。
在每个用户拥有唯一用户名的网站上,两个用户可能会尝试同时创建具有相同用户名的帐户。您可以使用事务来检查名称是否被采用,如果没有,则使用该名称创建账户。但是,像在前面的例子中那样,在快照隔离下是不安全的。幸运的是,一个唯一的约束是一个简单的解决方案(第二个事务,试图注册用户名将被中止,由于违反约束)。
***防止双重开支***
允许用户花钱或积分的服务需要检查用户花费的不多。你可以通过在用户的帐户中插入一个消费项目来实现这一点,列出帐户中的所有项目,并检查总和是否为正值[44]。有了写歪斜,可能会发生两个支出项目同时插入,一起导致余额变为负值,但这两个交易都不会注意到另一个。
允许用户花钱或积分的服务需要检查用户花费的不多。你可以通过在用户的帐户中插入一个消费项目来实现这一点,列出帐户中的所有项目,并检查总和是否为正值[44]。有了写歪斜,可能会发生两个支出项目同时插入,一起导致余额变为负值,但这两个事务都不会注意到另一个。
[^ix]: 在PostgreSQL中您可以使用范围类型更优雅地执行此操作但在其他数据库中并未得到广泛支持。
@ -549,7 +578,7 @@ COMMIT;
## 串行执行
在本章中,我们已经看到了几个易于出现竞争条件的交易的例子。读取提交和快照隔离级别会阻止某些竞争条件,但其他竞争条件则不会。我们遇到了一些特别棘手的例子,写有歪斜和幻影。这是一个可悲的情况:
在本章中,我们已经看到了几个易于出现竞争条件的事务的例子。读取提交和快照隔离级别会阻止某些竞争条件,但其他竞争条件则不会。我们遇到了一些特别棘手的例子,写有歪斜和幻影。这是一个可悲的情况:
- 隔离级别难以理解,并且在不同的数据库中不一致地实现(例如,“可重复读取”的含义差别很大)。
- 如果您查看应用程序代码,很难判断在特定的隔离级别运行是否安全 - 特别是在大型应用程序中,您可能并不知道可能同时发生的所有事情。
@ -583,13 +612,13 @@ COMMIT;
- RAM变得足够便宜现在许多用例可以将整个活动数据集保存在内存中请参阅第88页的“将所有内容保留在内存中”。当事务需要访问的所有数据都在内存中时事务处理的执行速度要比等待数据从磁盘加载时快得多。
- 数据库设计人员意识到OLTP事务通常很短只能进行少量的读写操作请参阅“事务处理或分析。相比之下长时间运行的分析查询通常是只读的因此它们可以在串行执行循环之外的一致快照使用快照隔离上运行。
串行执行事务的方法在VoltDB / H-StoreRedis和Datomic中实现[46,47,48]。设计用于单线程执行的系统有时可以比支持并发的系统更好因为它可以避免锁定的协调开销。但是其吞吐量仅限于单个CPU内核的吞吐量。为了充分利用单一线索交易需要与传统形式不同的结构。
串行执行事务的方法在VoltDB / H-StoreRedis和Datomic中实现[46,47,48]。设计用于单线程执行的系统有时可以比支持并发的系统更好因为它可以避免锁定的协调开销。但是其吞吐量仅限于单个CPU内核的吞吐量。为了充分利用单一线索事务需要与传统形式不同的结构。
#### 在存储过程中封装事务
在数据库的早期阶段,意图是数据库事务可以包含整个用户活动流程。例如,预订机票是一个多阶段的过程(搜索路线,票价和可用座位,决定行程,在行程的每个航班上预订座位,输入旅客详细信息,付款)。数据库设计者认为,如果整个过程是一个事务,那么它就可以被原子化地执行。
不幸的是人类做出决定和回应的速度非常缓慢。如果数据库事务需要等待来自用户的输入则数据库需要支持潜在的大量并发事务其中大部分是空闲的。大多数数据库不能高效地完成这项工作因此几乎所有的OLTP应用程序都通过避免交互式地等待交易中的用户来保持交易的简短。在Web上这意味着事务在同一个HTTP请求中被提交 - 一个事务不会跨越多个请求。一个新的HTTP请求开始一个新的事务。
不幸的是人类做出决定和回应的速度非常缓慢。如果数据库事务需要等待来自用户的输入则数据库需要支持潜在的大量并发事务其中大部分是空闲的。大多数数据库不能高效地完成这项工作因此几乎所有的OLTP应用程序都通过避免交互式地等待事务中的用户来保持事务的简短。在Web上这意味着事务在同一个HTTP请求中被提交 - 一个事务不会跨越多个请求。一个新的HTTP请求开始一个新的事务。
即使人类已经找出了关键路径,事务仍然以交互式的客户端/服务器风格执行,一次一个语句。应用程序进行查询,读取结果,可能根据第一个查询的结果进行另一个查询,依此类推。查询和结果在应用程序代码(在一台机器上运行)和数据库服务器(在另一台机器上)之间来回发送。
@ -631,10 +660,10 @@ VoltDB还使用存储过程进行复制不是将事务的写入从一个节
事务的串行执行已成为在一定的约束条件下实现可序列化的隔离的一种可行方法:
- 每笔交易都必须小而快,因为只需一个缓慢的交易即可拖延所有交易处理。
- 每笔事务都必须小而快,因为只需一个缓慢的事务即可拖延所有事务处理。
- 仅限于使用活动数据集可以放入内存的情况。很少访问的数据可能会被移动到磁盘,但是如果需要在单线程事中访问,系统会变得非常慢[^x]。
- 写入吞吐量必须足够低才能在单个CPU内核上处理否则事务需要进行分区而不需要跨分区协调。
- 交叉分区交易是可能的,但是它们的使用程度有很大的限制。
- 交叉分区事务是可能的,但是它们的使用程度有很大的限制。
[^x]: 如果事务需要访问不在内存中的数据最好的解决方案可能是放弃事务异步地将数据提取到内存中同时继续处理其他事务然后在数据加载时重新启动事务。这种方法被称为反高速缓存正如前面在第88页“将所有内容保存在内存”中所述。
@ -646,7 +675,7 @@ VoltDB还使用存储过程进行复制不是将事务的写入从一个节
>
> 请注意虽然两阶段锁定2PL听起来非常类似于两阶段提交2PC但它们是完全不同的东西。我们将在第9章讨论2PC。
之前我们看到锁通常用于防止脏写入请参阅“无脏写”一节第217页如果两个事务同时尝试写入同一个对象则锁可确保第二个写入器必须等到第一个写入器完成交易(中止或承诺),然后才能继续。
之前我们看到锁通常用于防止脏写入请参阅“无脏写”一节第217页如果两个事务同时尝试写入同一个对象则锁可确保第二个写入器必须等到第一个写入器完成事务(中止或承诺),然后才能继续。
两相锁定类似,但使锁定要求更强。只要没有人写信,就允许多个事务同时读取同一个对象。但只要有人想写(修改或删除)对象,就需要独占访问权限:
@ -676,7 +705,7 @@ VoltDB还使用存储过程进行复制不是将事务的写入从一个节
这部分是由于获取和释放所有这些锁的开销,但更重要的是由于并发性的降低。按照设计,如果两个并发事务试图做任何可能导致竞争条件的事情,那么必须等待另一个完成。
传统的关系数据库不限制事务的持续时间,因为它们是为等待人类输入的交互式应用而设计的。因此,当一个交易需要等待另一个交易时,可以等待多长时间没有限制。即使你保证你所有的交易都是短暂的,如果有多个交易想要访问同一个对象,那么可能会形成一个队列,所以交易可能需要等待几个其他交易才能完成。
传统的关系数据库不限制事务的持续时间,因为它们是为等待人类输入的交互式应用而设计的。因此,当一个事务需要等待另一个事务时,可以等待多长时间没有限制。即使你保证你所有的事务都是短暂的,如果有多个事务想要访问同一个对象,那么可能会形成一个队列,所以事务可能需要等待几个其他事务才能完成。
因此运行2PL的数据库可能具有相当不稳定的等待时间如果在工作负载中存在争用那么在高百分比情况下它们可能非常慢请参阅第13页的“描述性能”。它可能只需要一个缓慢的事务或者一个访问大量数据并获取许多锁的事务导致系统的其他部分停下来。当需要稳健的操作时这种不稳定性是有问题的。
@ -686,7 +715,7 @@ VoltDB还使用存储过程进行复制不是将事务的写入从一个节
在前面关于锁的描述中我们掩盖了一个微妙而重要的细节。在第250页的“导致写入歪斜的幻像”中我们讨论了一些性能问题即一个事务改变另一个事务的搜索查询的结果。具有可序列化隔离的数据库必须防止幻像。
在会议室预订的例子中,这意味着如果一个交易在某个时间窗口内搜索了一个房间的现有预订见例7-2则另一个交易不能同时插入或更新同一房间的另一个预订,时间范围。 (可以同时插入其他房间的预订,或在不影响预定预订的不同时间的同一房间)。
在会议室预订的例子中,这意味着如果一个事务在某个时间窗口内搜索了一个房间的现有预订见例7-2则另一个事务不能同时插入或更新同一房间的另一个预订,时间范围。 (可以同时插入其他房间的预订,或在不影响预定预订的不同时间的同一房间)。
我们如何实现这一点?从概念上讲,我们需要一个谓词锁定[3]。它类似于前面描述的共享/排它锁,但不属于特定的对象(例如,表中的一行),它属于所有符合某些搜索条件的对象,如:
@ -712,14 +741,14 @@ WHERE room_id = 123 AND
在房间预订数据库中您可能会在room_id列上有一个索引并且/或者在start_time和end_time上有索引否则前面的查询在大型数据库上的速度会非常慢
- 假设您的索引位于room_id上并且数据库使用此索引查找123号房间的现有预订。现在数据库可以简单地将共享锁附加到此索引条目指示交易已搜索123号房间的预订。
- 或者,如果数据库使用基于时间的索引来查找现有预订,那么它可以将共享锁附加到该索引中的一系列值,指示交易已搜索与中午的时间段重叠的预订到下午1点2018年1月1日
- 假设您的索引位于room_id上并且数据库使用此索引查找123号房间的现有预订。现在数据库可以简单地将共享锁附加到此索引条目指示事务已搜索123号房间的预订。
- 或者,如果数据库使用基于时间的索引来查找现有预订,那么它可以将共享锁附加到该索引中的一系列值,指示事务已搜索与中午的时间段重叠的预订到下午1点2018年1月1日
无论哪种方式,搜索条件的近似值都附加到其中一个索引上。现在,如果另一个交易想要插入,更新或删除同一个房间和/或重叠时间段的预订,则它将不得不更新索引的相同部分。在这样做的过程中,它会遇到共享锁,它将被迫等到锁被释放。
无论哪种方式,搜索条件的近似值都附加到其中一个索引上。现在,如果另一个事务想要插入,更新或删除同一个房间和/或重叠时间段的预订,则它将不得不更新索引的相同部分。在这样做的过程中,它会遇到共享锁,它将被迫等到锁被释放。
这提供了有效的防止幻影和写歪斜。索引范围锁并不像谓词锁那样精确(它们可能会锁定更大范围的对象,而不是维持可串行化所必需的范围),但是由于它们的开销较低,所以它们是一个很好的折衷。
如果在可以连接范围锁的地方没有合适的索引,则数据库可以回退到整个表上的共享锁。这对性能不利,因为它会阻止所有其他交易写入表格,但这是一个安全的回退位置。
如果在可以连接范围锁的地方没有合适的索引,则数据库可以回退到整个表上的共享锁。这对性能不利,因为它会阻止所有其他事务写入表格,但这是一个安全的回退位置。
@ -735,22 +764,22 @@ WHERE room_id = 123 AND
两阶段锁定是一种所谓的悲观并发控制机制:它是基于如果有什么可能出错(如另一个事务所持有的锁所表示的)的原则,最好等到情况再次安全做任何事情。这就像互斥,用于保护多线程编程中的数据结构。
从某种意义上说,串行执行是极端悲观的:在事务持续期间,每个事务对整个数据库(或数据库的一个分区)具有独占锁定,这基本相当。我们通过使每笔交易执行得非常快来弥补悲观情绪,所以只需要短时间保持“锁定”。
从某种意义上说,串行执行是极端悲观的:在事务持续期间,每个事务对整个数据库(或数据库的一个分区)具有独占锁定,这基本相当。我们通过使每笔事务执行得非常快来弥补悲观情绪,所以只需要短时间保持“锁定”。
相比之下,可串行化的快照隔离是一种乐观的并发控制技术。在这种情况下乐观意味着,如果发生某种可能的危险,不要阻止交易,反而继续交易,希望一切都会好起来。当一个事务想要提交时,数据库检查是否有什么不好的事情发生(即隔离是否被违反);如果是的话,交易将被中止,并且必须重试。只有可序列化的事务才被允许提交。
相比之下,可串行化的快照隔离是一种乐观的并发控制技术。在这种情况下乐观意味着,如果发生某种可能的危险,不要阻止事务,反而继续事务,希望一切都会好起来。当一个事务想要提交时,数据库检查是否有什么不好的事情发生(即隔离是否被违反);如果是的话,事务将被中止,并且必须重试。只有可序列化的事务才被允许提交。
乐观并发控制是一个古老的想法[52],其优点和缺点已经争论了很长时间[53]。如果存在较高的意图(很多事务试图访问相同的对象),则表现不佳,因为这会导致很大一部分事务需要中止。如果系统已经接近最大吞吐量,来自重试交易的额外交易负载可能会使性能变差。
乐观并发控制是一个古老的想法[52],其优点和缺点已经争论了很长时间[53]。如果存在较高的意图(很多事务试图访问相同的对象),则表现不佳,因为这会导致很大一部分事务需要中止。如果系统已经接近最大吞吐量,来自重试事务的额外事务负载可能会使性能变差。
但是,如果有足够的备用容量,并且交易之间的争用不是太高,乐观的并发控制技术往往比悲观的要好。可交换的原子操作可以减少争用:例如,如果多个事务同时要增加一个计数器,那么应用增量的顺序(只要计数器不在同一个事务中读取)就无关紧要了,所以并发增量可以全部应用而不冲突。
但是,如果有足够的备用容量,并且事务之间的争用不是太高,乐观的并发控制技术往往比悲观的要好。可交换的原子操作可以减少争用:例如,如果多个事务同时要增加一个计数器,那么应用增量的顺序(只要计数器不在同一个事务中读取)就无关紧要了,所以并发增量可以全部应用而不冲突。
顾名思义SSI基于快照隔离 - 也就是说事务中的所有读取操作都是通过数据库的一致快照创建的请参见第237页的“快照隔离和可重复读取”。与早期的乐观并发控制技术相比这是主要的区别。在快照隔离的基础上SSI添加了一种算法来检测写入之间的序列化冲突并确定要中止哪些事务。
#### 基于过时前提的决定
当我们先前讨论了快照隔离中的写入歪斜请参阅“写入歪斜和幻像”第221页我们观察到一个循环模式事务从数据库读取一些数据检查查询的结果并决定采取一些操作写入数据库根据它看到的结果。但是在快照隔离的情况下原始查询的结果在事务提交时可能不再是最新的因为数据可能在同一时间被修改。
换句话说,交易正在基于一个前提采取行动(交易一开始就是事实,例如“目前有两名医生正在通话”)。之后,当交易要提交时,原始数据可能已经改变 - 前提可能不再成立。
换句话说,事务正在基于一个前提采取行动(事务一开始就是事实,例如“目前有两名医生正在通话”)。之后,当事务要提交时,原始数据可能已经改变 - 前提可能不再成立。
当应用程序进行查询时(例如,“当前有多少医生正在调用?”),数据库不知道应用程序逻辑如何使用该查询的结果。为了安全,数据在这种情况下,交易可能在一个过时的前提下采取了行动并放弃交易
当应用程序进行查询时(例如,“当前有多少医生正在调用?”),数据库不知道应用程序逻辑如何使用该查询的结果。为了安全,数据在这种情况下,事务可能在一个过时的前提下采取了行动并放弃事务
数据库如何知道查询结果是否可能已经改变?有两种情况需要考虑:
@ -765,9 +794,9 @@ WHERE room_id = 123 AND
**图7-10 检测事务何时从MVCC快照读取过时的值**
为了防止这种异常数据库需要跟踪一个事务由于MVCC可见性规则而忽略另一个事务的写入。当事务想要提交时数据库检查是否有任何被忽略的写入现在已经被提交。如果是这样交易必须中止。
为了防止这种异常数据库需要跟踪一个事务由于MVCC可见性规则而忽略另一个事务的写入。当事务想要提交时数据库检查是否有任何被忽略的写入现在已经被提交。如果是这样事务必须中止。
为什么要等到提交当检测到陈旧的读取时为什么不立即中止事务43那么如果事务43是只读事务则不需要中止因为没有写歪斜的风险。当事务43进行读取时数据库还不知道事务是否要稍后执行写操作。此外交易42可能在交易43被提交的时候中止或者可能仍然未被提交因此读取可能终究不是陈旧的。通过避免不必要的中止SSI保留快照隔离对从一致快照中长时间运行的读取的支持。
为什么要等到提交当检测到陈旧的读取时为什么不立即中止事务43那么如果事务43是只读事务则不需要中止因为没有写歪斜的风险。当事务43进行读取时数据库还不知道事务是否要稍后执行写操作。此外事务42可能在事务43被提交的时候中止或者可能仍然未被提交因此读取可能终究不是陈旧的。通过避免不必要的中止SSI保留快照隔离对从一致快照中长时间运行的读取的支持。
#### 检测影响之前读取的写入
@ -779,15 +808,15 @@ WHERE room_id = 123 AND
在两阶段锁定的上下文中我们讨论了索引范围锁请参阅“索引范围锁”第259页它允许数据库锁定与某个搜索查询匹配的所有行的访问权例如WHERE shift_id = 1234。可以在这里使用类似的技术除了SSI锁不会阻塞其他事务。
在图7-11中交易42和43都在轮班1234期间寻找在线医生。如果在shift_id上有索引则数据库可以使用索引条目1234来记录事务42和43读取这个数据的事实。 (如果没有索引,这个信息可以在表级进行跟踪)。这个信息只需要保留一段时间:在一个事务完成(提交或中止)之后,所有的并发事务完成之后,数据库可能会忘记它读取的数据。
在图7-11中事务42和43都在轮班1234期间寻找在线医生。如果在shift_id上有索引则数据库可以使用索引条目1234来记录事务42和43读取这个数据的事实。 (如果没有索引,这个信息可以在表级进行跟踪)。这个信息只需要保留一段时间:在一个事务完成(提交或中止)之后,所有的并发事务完成之后,数据库可能会忘记它读取的数据。
当事务写入数据库时它必须在索引中查找最近读取受影响数据的其他事务。这个过程类似于在受影响的密钥范围上获取写入锁定而不是在读取器提交之前阻塞锁定作为tripwire它只是通知事务他们读取的数据可能不再是最新的。
在图7-11中交易43通知交易42其先前的阅读已过时反之亦然。事务42首先提交并且成功尽管事务43的写入受到影响42,43尚未提交所以写入尚未生效。然而当事务43想要提交时来自42的冲突写入已经被提交所以43必须中止。
在图7-11中事务43通知事务42其先前的阅读已过时反之亦然。事务42首先提交并且成功尽管事务43的写入受到影响42,43尚未提交所以写入尚未生效。然而当事务43想要提交时来自42的冲突写入已经被提交所以43必须中止。
#### 可序列化的快照隔离的性能
与往常一样,许多工程细节会影响算法在实践中的效果。例如,一个权衡是跟踪事务的读取和写入的粒度。如果数据库非常详细地跟踪每个事务的活动,那么可以准确地确定哪些事务需要中止,但是记帐开销可能变得很重要。不太详细的跟踪速度更快,但可能会导致更多的交易被中止。
与往常一样,许多工程细节会影响算法在实践中的效果。例如,一个权衡是跟踪事务的读取和写入的粒度。如果数据库非常详细地跟踪每个事务的活动,那么可以准确地确定哪些事务需要中止,但是记帐开销可能变得很重要。不太详细的跟踪速度更快,但可能会导致更多的事务被中止。
在某些情况下,事务可以读取被另一个事务覆盖的信息:取决于发生了什么,有时可以证明执行的结果是可序列化的。 PostgreSQL使用这个理论来减少不必要的中止次数[11,41]。
@ -803,7 +832,7 @@ WHERE room_id = 123 AND
事务是一个抽象层,允许应用程序假装某些并发问题和某些类型的硬件和软件故障不存在。大量的错误被简化为简单的事务中止,而应用程序只需要再次尝试。
在本章中,我们看到许多交易有助于防止的问题。并非所有的应用程序都容易出现所有这些问题:具有非常简单的访问模式的应用程序(例如只读和写一条记录)可能无需事务管理。但是,对于更复杂的访问模式,事务可以大大减少您需要考虑的潜在错误情况的数量。
在本章中,我们看到许多事务有助于防止的问题。并非所有的应用程序都容易出现所有这些问题:具有非常简单的访问模式的应用程序(例如只读和写一条记录)可能无需事务管理。但是,对于更复杂的访问模式,事务可以大大减少您需要考虑的潜在错误情况的数量。
没有事务处理,各种错误情况(进程崩溃,网络中断,停电,磁盘已满,意外并发等)意味着数据可能以各种方式变得不一致。例如,非规格化的数据可能很容易与源数据不同步。如果没有事务处理,就很难推断复杂的交互访问可能对数据库造成的影响。
@ -832,7 +861,7 @@ WHERE room_id = 123 AND
弱隔离级别可以防止这些异常情况,但是让应用程序开发人员手动处理其他应用程序(例如,使用显式锁定)。只有可序列化的隔离才能防范所有这些问题。我们讨论了实现可序列化事务的三种不同方法:
***按照连续顺序从字面上执行交易***
***按照连续顺序从字面上执行事务***
如果您可以使每个事务的执行速度非常快并且事务吞吐量足够低以在单个CPU内核上处理这是一个简单而有效的选择。
***两相锁定***
@ -1007,7 +1036,6 @@ WHERE room_id = 123 AND
*blog.foundationdb.com*, December 10, 2014.
------
| 上一章 | 目录 | 下一章 |