mirror of
https://github.com/Vonng/ddia.git
synced 2024-12-06 15:20:12 +08:00
Update ch7.md
This commit is contained in:
parent
8315561af4
commit
d17942ff93
58
ch7.md
58
ch7.md
@ -21,7 +21,7 @@
|
||||
|
||||
为了实现可靠性,系统必须处理这些故障,确保它们不会导致整个系统的灾难性故障。但是实现容错机制工作量巨大。需要仔细考虑所有可能出错的事情,并进行大量的测试,以确保解决方案真正管用。
|
||||
|
||||
数十年来,**事务(transaction)**一直是简化这些问题的首选机制。事务是应用程序将多个读写操作组合成一个逻辑单元的一种方式。从概念上讲,事务中的所有读写操作被视作单个操作来执行:整个事务要么成功(**提交(commit)**)要么失败(**中止(abort)**,**回滚(rollback)**)。如果失败,应用程序可以安全地重试。对于事务来说,应用程序的错误处理变得简单多了,因为它不用再担心部分失败的情况了,即某些操作成功,某些失败(无论出于何种原因)。
|
||||
数十年来,**事务(transaction)** 一直是简化这些问题的首选机制。事务是应用程序将多个读写操作组合成一个逻辑单元的一种方式。从概念上讲,事务中的所有读写操作被视作单个操作来执行:整个事务要么成功(**提交(commit)**)要么失败(**中止(abort)**,**回滚(rollback)**)。如果失败,应用程序可以安全地重试。对于事务来说,应用程序的错误处理变得简单多了,因为它不用再担心部分失败的情况了,即某些操作成功,某些失败(无论出于何种原因)。
|
||||
|
||||
和事务打交道时间长了,你可能会觉得它显而易见。但我们不应将其视为理所当然。事务不自然法;它们是为了**简化应用编程模型**而创建的。通过使用事务,应用程序可以自由地忽略某些潜在的错误情况和并发问题,因为数据库会替应用处理好这些。(我们称之为**安全保证(safety guarantees)**)。
|
||||
|
||||
@ -49,7 +49,7 @@
|
||||
|
||||
事务所提供的安全保证,通常由众所周知的首字母缩略词ACID来描述,ACID代表**原子性(Atomicity)**,**一致性(Consistency)**,**隔离性(Isolation)**和**持久性(Durability)**。它由TheoHärder和Andreas Reuter于1983年创建,旨在为数据库中的容错机制建立精确的术语。
|
||||
|
||||
但实际上,不同数据库的ACID实现并不相同。例如,我们将会看到,围绕着**隔离性(Isolation)**的含义有许多含糊不清【8】。高层次上的想法是合理的,但魔鬼隐藏在细节里。今天,当一个系统声称自己“符合ACID”时,实际上能期待的是什么保证并不清楚。不幸的是,ACID现在几乎已经变成了一个营销术语。
|
||||
但实际上,不同数据库的ACID实现并不相同。例如,我们将会看到,围绕着**隔离性(Isolation)** 的含义有许多含糊不清【8】。高层次上的想法是合理的,但魔鬼隐藏在细节里。今天,当一个系统声称自己“符合ACID”时,实际上能期待的是什么保证并不清楚。不幸的是,ACID现在几乎已经变成了一个营销术语。
|
||||
|
||||
(不符合ACID标准的系统有时被称为BASE,它代表**基本可用性(Basically Available)**,**软状态(Soft State)**和**最终一致性(Eventual consistency)**【9】,这比ACID的定义更加模糊,似乎BASE的唯一合理的定义是“不是ACID”,即它几乎可以代表任何你想要的东西。)
|
||||
|
||||
@ -59,13 +59,13 @@
|
||||
|
||||
一般来说,原子是指不能分解成小部分的东西。这个词在计算的不同分支中意味着相似但又微妙不同的东西。例如,在多线程编程中,如果一个线程执行一个原子操作,这意味着另一个线程无法看到该操作的一半结果。系统只能处于操作之前或操作之后的状态,而不是介于两者之间的状态。
|
||||
|
||||
相比之下,ACID的原子性并**不**是关于**并发(concurrent)**的。它并不是在描述如果几个进程试图同时访问相同的数据会发生什么情况,这种情况包含在缩写***I***中,即[**隔离性(Isolation)**](#隔离性(Isolation))
|
||||
相比之下,ACID的原子性并**不**是关于**并发(concurrent)**的。它并不是在描述如果几个进程试图同时访问相同的数据会发生什么情况,这种情况包含在缩写***I*** 中,即[**隔离性(Isolation)**](#隔离性(Isolation))
|
||||
|
||||
ACID的原子性描述了,当客户想进行多次写入,但在一些写操作处理完之后出现故障的情况。例如进程崩溃,网络连接中断,磁盘变满或者某种完整性约束被违反。如果这些写操作被分组到一个原子事务中,并且该事务由于错误而不能完成(提交),则该事务将被中止,并且数据库必须丢弃或撤消该事务中迄今为止所做的任何写入。
|
||||
|
||||
如果没有原子性,在多处更改进行到一半时发生错误,很难知道哪些更改已经生效,哪些没有生效。该应用程序可以再试一次,但冒着进行两次相同变更的风险,可能会导致数据重复或错误的数据。原子性简化了这个问题:如果事务被**中止(abort)**,应用程序可以确定它没有改变任何东西,所以可以安全地重试。
|
||||
|
||||
ACID原子性的定义特征是:**能够在错误时中止事务,丢弃该事务进行的所有写入变更的能力。**或许**可中止性(abortability)**是更好的术语,但本书将继续使用原子性,因为这是惯用词。
|
||||
ACID原子性的定义特征是:**能够在错误时中止事务,丢弃该事务进行的所有写入变更的能力。** 或许 **可中止性(abortability)** 是更好的术语,但本书将继续使用原子性,因为这是惯用词。
|
||||
|
||||
#### 一致性(Consistency)
|
||||
|
||||
@ -98,15 +98,15 @@ ACID意义上的隔离性意味着,**同时执行的事务是相互隔离的**
|
||||
|
||||
**图7-1 两个客户之间的竞争状态同时递增计数器**
|
||||
|
||||
然而实践中很少会使用可序列化隔离,因为它有性能损失。一些流行的数据库如Oracle 11g,甚至没有实现它。在Oracle中有一个名为“可序列化”的隔离级别,但实际上它实现了一种叫做**快照隔离(snapshot isolation)**的功能,**这是一种比可序列化更弱的保证**【8,11】。我们将在“[弱隔离等级]()”中研究快照隔离和其他形式的隔离。
|
||||
然而实践中很少会使用可序列化隔离,因为它有性能损失。一些流行的数据库如Oracle 11g,甚至没有实现它。在Oracle中有一个名为“可序列化”的隔离级别,但实际上它实现了一种叫做**快照隔离(snapshot isolation)** 的功能,**这是一种比可序列化更弱的保证**【8,11】。我们将在“[弱隔离等级]()”中研究快照隔离和其他形式的隔离。
|
||||
|
||||
#### 持久性(Durability)
|
||||
|
||||
数据库系统的目的是,提供一个安全的地方存储数据,而不用担心丢失。**持久性**是一个承诺,即一旦事务成功完成,即使发生硬件故障或数据库崩溃,写入的任何数据也不会丢失。
|
||||
数据库系统的目的是,提供一个安全的地方存储数据,而不用担心丢失。**持久性** 是一个承诺,即一旦事务成功完成,即使发生硬件故障或数据库崩溃,写入的任何数据也不会丢失。
|
||||
|
||||
在单节点数据库中,持久性通常意味着数据已被写入非易失性存储设备,如硬盘或SSD。它通常还包括预写日志或类似的文件(参阅“[让B树更可靠](ch3.md#让B树更可靠)”),以便在磁盘上的数据结构损坏时进行恢复。在带复制的数据库中,持久性可能意味着数据已成功复制到一些节点。为了提供持久性保证,数据库必须等到这些写入或复制完成后,才能报告事务成功提交。
|
||||
|
||||
如“[可靠性](ch1.md#可靠性)”一节所述,**完美的持久性是不存在的**:如果所有硬盘和所有备份同时被销毁,那显然没有任何数据库能救得了你。
|
||||
如“[可靠性](ch1.md#可靠性)”一节所述,**完美的持久性是不存在的** :如果所有硬盘和所有备份同时被销毁,那显然没有任何数据库能救得了你。
|
||||
|
||||
> #### 复制和持久性
|
||||
>
|
||||
@ -137,7 +137,7 @@ ACID意义上的隔离性意味着,**同时执行的事务是相互隔离的**
|
||||
|
||||
同时运行的事务不应该互相干扰。例如,如果一个事务进行多次写入,则另一个事务要么看到全部写入结果,要么什么都看不到,但不应该是一些子集。
|
||||
|
||||
这些定义假设你想同时修改多个对象(行,文档,记录)。通常需要**多对象事务(multi-object transaction)**来保持多块数据同步。[图7-2](img/fig7-2.png)展示了一个来自电邮应用的例子。执行以下查询来显示用户未读邮件数量:
|
||||
这些定义假设你想同时修改多个对象(行,文档,记录)。通常需要**多对象事务(multi-object transaction)** 来保持多块数据同步。[图7-2](img/fig7-2.png)展示了一个来自电邮应用的例子。执行以下查询来显示用户未读邮件数量:
|
||||
|
||||
```sql
|
||||
SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
@ -175,11 +175,11 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
这些问题非常让人头大,故存储引擎一个几乎普遍的目标是:对单节点上的单个对象(例如键值对)上提供原子性和隔离性。原子性可以通过使用日志来实现崩溃恢复(参阅“[使B树可靠]()”),并且可以使用每个对象上的锁来实现隔离(每次只允许一个线程访问对象) )。
|
||||
|
||||
一些数据库也提供更复杂的原子操作,例如自增操作,这样就不再需要像 [图7-1](img/fig7-1.png) 那样的读取-修改-写入序列了。同样流行的是**[比较和设置(CAS, compare-and-set)](#比较并设置(CAS))**操作,当值没有并发被其他人修改过时,才允许执行写操作。
|
||||
一些数据库也提供更复杂的原子操作,例如自增操作,这样就不再需要像 [图7-1](img/fig7-1.png) 那样的读取-修改-写入序列了。同样流行的是 **[比较和设置(CAS, compare-and-set)](#比较并设置(CAS))** 操作,当值没有并发被其他人修改过时,才允许执行写操作。
|
||||
|
||||
这些单对象操作很有用,因为它们可以防止在多个客户端尝试同时写入同一个对象时丢失更新(参阅“[防止丢失更新](#防止丢失更新)”)。但它们不是通常意义上的事务。CAS以及其他单一对象操作被称为“轻量级事务”,甚至出于营销目的被称为“ACID”【20,21,22】,但是这个术语是误导性的。事务通常被理解为,**将多个对象上的多个操作合并为一个执行单元的机制**。[^iv]
|
||||
|
||||
[^iv]: 严格地说,**原子自增(atomic increment)**这个术语在多线程编程的意义上使用了原子这个词。 在ACID的情况下,它实际上应该被称为**孤立(isolated)**的或**可序列化(serializable)**的增量。 但这就太吹毛求疵了。
|
||||
[^iv]: 严格地说,**原子自增(atomic increment)** 这个术语在多线程编程的意义上使用了原子这个词。 在ACID的情况下,它实际上应该被称为 **孤立(isolated)** 的或**可序列化(serializable)** 的增量。 但这就太吹毛求疵了。
|
||||
|
||||
#### 多对象事务的需求
|
||||
|
||||
@ -201,14 +201,14 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
然而并不是所有的系统都遵循这个哲学。特别是具有[无主复制](ch6.md#无主复制)的数据存储,主要是在“尽力而为”的基础上进行工作。可以概括为“数据库将做尽可能多的事,运行遇到错误时,它不会撤消它已经完成的事情“ ——所以,从错误中恢复是应用程序的责任。
|
||||
|
||||
错误发生不可避免,但许多软件开发人员倾向于只考虑乐观情况,而不是错误处理的复杂性。例如,像Rails的ActiveRecord和Django这样的**对象关系映射(ORM, object-relation Mapping)**框架不会重试中断的事务—— 这个错误通常会导致一个从堆栈向上传播的异常,所以任何用户输入都会被丢弃,用户拿到一个错误信息。这实在是太耻辱了,因为中止的重点就是允许安全的重试。
|
||||
错误发生不可避免,但许多软件开发人员倾向于只考虑乐观情况,而不是错误处理的复杂性。例如,像Rails的ActiveRecord和Django这样的**对象关系映射(ORM, object-relation Mapping)** 框架不会重试中断的事务—— 这个错误通常会导致一个从堆栈向上传播的异常,所以任何用户输入都会被丢弃,用户拿到一个错误信息。这实在是太耻辱了,因为中止的重点就是允许安全的重试。
|
||||
|
||||
尽管重试一个中止的事务是一个简单而有效的错误处理机制,但它并不完美:
|
||||
|
||||
- 如果事务实际上成功了,但是在服务器试图向客户端确认提交成功时网络发生故障(所以客户端认为提交失败了),那么重试事务会导致事务被执行两次——除非你有一个额外的应用级除重机制。
|
||||
- 如果错误是由于负载过大造成的,则重试事务将使问题变得更糟,而不是更好。为了避免这种正反馈循环,可以限制重试次数,使用指数退避算法,并单独处理与过载相关的错误(如果允许)。
|
||||
- 仅在临时性错误(例如,由于死锁,异常情况,临时性网络中断和故障转移)后才值得重试。在发生永久性错误(例如,违反约束)之后重试是毫无意义的。
|
||||
- 如果事务在数据库之外也有副作用,即使事务被中止,也可能发生这些副作用。例如,如果你正在发送电子邮件,那你肯定不希望每次重试事务时都重新发送电子邮件。如果你想确保几个不同的系统一起提交或放弃,**二阶段提交(2PC, two-phase commit)**可以提供帮助(“[原子提交和两阶段提交(2PC)](ch9.md#原子提交与二阶段提交(2PC))”中将讨论这个问题)。
|
||||
- 如果事务在数据库之外也有副作用,即使事务被中止,也可能发生这些副作用。例如,如果你正在发送电子邮件,那你肯定不希望每次重试事务时都重新发送电子邮件。如果你想确保几个不同的系统一起提交或放弃,**二阶段提交(2PC, two-phase commit)** 可以提供帮助(“[原子提交和两阶段提交(2PC)](ch9.md#原子提交与二阶段提交(2PC))”中将讨论这个问题)。
|
||||
- 如果客户端进程在重试中失效,任何试图写入数据库的数据都将丢失。
|
||||
|
||||
|
||||
@ -217,15 +217,15 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
## 弱隔离级别
|
||||
|
||||
如果两个事务不触及相同的数据,它们可以安全地**并行(parallel)**运行,因为两者都不依赖于另一个。当一个事务读取由另一个事务同时修改的数据时,或者当两个事务试图同时修改相同的数据时,并发问题(竞争条件)才会出现。
|
||||
如果两个事务不触及相同的数据,它们可以安全地**并行(parallel)** 运行,因为两者都不依赖于另一个。当一个事务读取由另一个事务同时修改的数据时,或者当两个事务试图同时修改相同的数据时,并发问题(竞争条件)才会出现。
|
||||
|
||||
并发BUG很难通过测试找到,因为这样的错误只有在特殊时机下才会触发。这样的时机可能很少,通常很难重现[^译注i]。并发性也很难推理,特别是在大型应用中,你不一定知道哪些其他代码正在访问数据库。在一次只有一个用户时,应用开发已经很麻烦了,有许多并发用户使得它更加困难,因为任何一个数据都可能随时改变。
|
||||
|
||||
[^译注i]: 轶事:偶然出现的瞬时错误有时称为***Heisenbug***,而确定性的问题对应地称为***Bohrbugs***
|
||||
|
||||
出于这个原因,数据库一直试图通过提供**事务隔离(transaction isolation)**来隐藏应用程序开发者的并发问题。从理论上讲,隔离可以通过假装没有并发发生,让你的生活更加轻松:**可序列化(serializable)**的隔离等级意味着数据库保证事务的效果与连续运行(即一次一个,没有任何并发)是一样的。
|
||||
出于这个原因,数据库一直试图通过提供**事务隔离(transaction isolation)** 来隐藏应用程序开发者的并发问题。从理论上讲,隔离可以通过假装没有并发发生,让你的生活更加轻松:**可序列化(serializable)** 的隔离等级意味着数据库保证事务的效果与连续运行(即一次一个,没有任何并发)是一样的。
|
||||
|
||||
实际上不幸的是:隔离并没有那么简单。**可序列化**会有性能损失,许多数据库不愿意支付这个代价【8】。因此,系统通常使用较弱的隔离级别来防止一部分,而不是全部的并发问题。这些隔离级别难以理解,并且会导致微妙的错误,但是它们仍然在实践中被使用【23】。
|
||||
实际上不幸的是:隔离并没有那么简单。**可序列化** 会有性能损失,许多数据库不愿意支付这个代价【8】。因此,系统通常使用较弱的隔离级别来防止一部分,而不是全部的并发问题。这些隔离级别难以理解,并且会导致微妙的错误,但是它们仍然在实践中被使用【23】。
|
||||
|
||||
并发性错误导致的并发性错误不仅仅是一个理论问题。它们造成了很多的资金损失【24,25】,耗费了财务审计人员的调查【26】,并导致客户数据被破坏【27】。关于这类问题的一个流行的评论是“如果你正在处理财务数据,请使用ACID数据库!” —— 但是这一点没有提到。即使是很多流行的关系型数据库系统(通常被认为是“ACID”)也使用弱隔离级别,所以它们也不一定能防止这些错误的发生。
|
||||
|
||||
@ -278,7 +278,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
**读已提交**是一个非常流行的隔离级别。这是Oracle 11g,PostgreSQL,SQL Server 2012,MemSQL和其他许多数据库的默认设置【8】。
|
||||
|
||||
最常见的情况是,数据库通过使用**行锁(row-level lock)**来防止脏写:当事务想要修改特定对象(行或文档)时,它必须首先获得该对象的锁。然后必须持有该锁直到事务被提交或中止。一次只有一个事务可持有任何给定对象的锁;如果另一个事务要写入同一个对象,则必须等到第一个事务提交或中止后,才能获取该锁并继续。这种锁定是读已提交模式(或更强的隔离级别)的数据库自动完成的。
|
||||
最常见的情况是,数据库通过使用**行锁(row-level lock)** 来防止脏写:当事务想要修改特定对象(行或文档)时,它必须首先获得该对象的锁。然后必须持有该锁直到事务被提交或中止。一次只有一个事务可持有任何给定对象的锁;如果另一个事务要写入同一个对象,则必须等到第一个事务提交或中止后,才能获取该锁并继续。这种锁定是读已提交模式(或更强的隔离级别)的数据库自动完成的。
|
||||
|
||||
如何防止脏读?一种选择是使用相同的锁,并要求任何想要读取对象的事务来简单地获取该锁,然后在读取之后立即再次释放该锁。这能确保不会读取进行时,对象不会在脏的状态,有未提交的值(因为在那段时间锁会被写入该对象的事务持有)。
|
||||
|
||||
@ -314,7 +314,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
有时,您可能需要运行一个查询,扫描大部分的数据库。这样的查询在分析中很常见(参阅“[事务处理或分析?](ch3.md#事务处理还是分析?)”),也可能是定期完整性检查(即监视数据损坏)的一部分。如果这些查询在不同时间点观察数据库的不同部分,则可能会返回毫无意义的结果。
|
||||
|
||||
**快照隔离(snapshot isolation)**【28】是这个问题最常见的解决方案。想法是,每个事务都从数据库的**一致快照(consistent snapshot)**中读取——也就是说,事务可以看到事务开始时在数据库中提交的所有数据。即使这些数据随后被另一个事务更改,每个事务也只能看到该特定时间点的旧数据。
|
||||
**快照隔离(snapshot isolation)**【28】是这个问题最常见的解决方案。想法是,每个事务都从数据库的**一致快照(consistent snapshot)** 中读取——也就是说,事务可以看到事务开始时在数据库中提交的所有数据。即使这些数据随后被另一个事务更改,每个事务也只能看到该特定时间点的旧数据。
|
||||
|
||||
快照隔离对长时间运行的只读查询(如备份和分析)非常有用。如果查询的数据在查询执行的同时发生变化,则很难理解查询的含义。当一个事务可以看到数据库在某个特定时间点冻结时的一致快照,理解起来就很容易了。
|
||||
|
||||
@ -366,7 +366,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
在实践中,许多实现细节决定了多版本并发控制的性能。例如,如果同一对象的不同版本可以放入同一个页面中,PostgreSQL的优化可以避免更新索引【31】。
|
||||
|
||||
在CouchDB,Datomic和LMDB中使用另一种方法。虽然它们也使用[B树](ch2.md#B树),但它们使用的是一种**仅追加/写时拷贝(append-only/copy-on-write)**的变体,它们在更新时不覆盖树的页面,而为每个修改页面创建一份副本。从父页面直到树根都会级联更新,以指向它们子页面的新版本。任何不受写入影响的页面都不需要被复制,并且保持不变【33,34,35】。
|
||||
在CouchDB,Datomic和LMDB中使用另一种方法。虽然它们也使用[B树](ch2.md#B树),但它们使用的是一种**仅追加/写时拷贝(append-only/copy-on-write)** 的变体,它们在更新时不覆盖树的页面,而为每个修改页面创建一份副本。从父页面直到树根都会级联更新,以指向它们子页面的新版本。任何不受写入影响的页面都不需要被复制,并且保持不变【33,34,35】。
|
||||
|
||||
使用仅追加的B树,每个写入事务(或一批事务)都会创建一颗新的B树,当创建时,从该特定树根生长的树就是数据库的一个一致性快照。没必要根据事务ID过滤掉对象,因为后续写入不能修改现有的B树;它们只能创建新的树根。但这种方法也需要一个负责压缩和垃圾收集的后台进程。
|
||||
|
||||
@ -384,9 +384,9 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
|
||||
到目前为止已经讨论的**读已提交**和**快照隔离**级别,主要保证了**只读事务在并发写入时**可以看到什么。却忽略了两个事务并发写入的问题——我们只讨论了[脏写](#脏写),一种特定类型的写-写冲突是可能出现的。
|
||||
|
||||
并发的写入事务之间还有其他几种有趣的冲突。其中最着名的是**丢失更新(lost update)**问题,如[图7-1]()所示,以两个并发计数器增量为例。
|
||||
并发的写入事务之间还有其他几种有趣的冲突。其中最着名的是**丢失更新(lost update)** 问题,如[图7-1]()所示,以两个并发计数器增量为例。
|
||||
|
||||
如果应用从数据库中读取一些值,修改它并写回修改的值(读取-修改-写入序列),则可能会发生丢失更新的问题。如果两个事务同时执行,则其中一个的修改可能会丢失,因为第二个写入的内容并没有包括第一个事务的修改(有时会说后面写入**狠揍(clobber)**了前面的写入)这种模式发生在各种不同的情况下:
|
||||
如果应用从数据库中读取一些值,修改它并写回修改的值(读取-修改-写入序列),则可能会发生丢失更新的问题。如果两个事务同时执行,则其中一个的修改可能会丢失,因为第二个写入的内容并没有包括第一个事务的修改(有时会说后面写入**狠揍(clobber)** 了前面的写入)这种模式发生在各种不同的情况下:
|
||||
|
||||
- 增加计数器或更新账户余额(需要读取当前值,计算新值并写回更新后的值)
|
||||
- 在复杂值中进行本地修改:例如,将元素添加到JSON文档中的一个列表(需要解析文档,进行更改并写回修改的文档)
|
||||
@ -579,7 +579,7 @@ COMMIT;
|
||||
|
||||
现在,要创建预订的事务可以锁定(`SELECT FOR UPDATE`)表中与所需房间和时间段对应的行。在获得锁定之后,它可以检查重叠的预订并像以前一样插入新的预订。请注意,这个表并不是用来存储预订相关的信息——它完全就是一组锁,用于防止同时修改同一房间和时间范围内的预订。
|
||||
|
||||
这种方法被称为**物化冲突(materializing conflicts)**,因为它将幻读变为数据库中一组具体行上的锁冲突【11】。不幸的是,弄清楚如何物化冲突可能很难,也很容易出错,而让并发控制机制泄漏到应用数据模型是很丑陋的做法。出于这些原因,如果没有其他办法可以实现,物化冲突应被视为最后的手段。在大多数情况下。**可序列化(Serializable)**的隔离级别是更可取的。
|
||||
这种方法被称为**物化冲突(materializing conflicts)**,因为它将幻读变为数据库中一组具体行上的锁冲突【11】。不幸的是,弄清楚如何物化冲突可能很难,也很容易出错,而让并发控制机制泄漏到应用数据模型是很丑陋的做法。出于这些原因,如果没有其他办法可以实现,物化冲突应被视为最后的手段。在大多数情况下。**可序列化(Serializable)** 的隔离级别是更可取的。
|
||||
|
||||
|
||||
|
||||
@ -591,7 +591,7 @@ COMMIT;
|
||||
- 光检查应用代码很难判断在特定的隔离级别运行是否安全。 特别是在大型应用程序中,您可能并不知道并发发生的所有事情。
|
||||
- 没有检测竞争条件的好工具。原则上来说,静态分析可能会有帮助【26】,但研究中的技术还没法实际应用。并发问题的测试是很难的,因为它们通常是非确定性的 —— 只有在倒霉的时机下才会出现问题。
|
||||
|
||||
这不是一个新问题,从20世纪70年代以来就一直是这样了,当时首先引入了较弱的隔离级别【2】。一直以来,研究人员的答案都很简单:使用**可序列化(serializable)**的隔离级别!
|
||||
这不是一个新问题,从20世纪70年代以来就一直是这样了,当时首先引入了较弱的隔离级别【2】。一直以来,研究人员的答案都很简单:使用**可序列化(serializable)** 的隔离级别!
|
||||
|
||||
**可序列化(Serializability)**隔离通常被认为是最强的隔离级别。它保证即使事务可以并行执行,最终的结果也是一样的,就好像它们没有任何并发性,连续挨个执行一样。因此数据库保证,如果事务在单独运行时正常运行,则它们在并发运行时继续保持正确 —— 换句话说,数据库可以防止**所有**可能的竞争条件。
|
||||
|
||||
@ -671,7 +671,7 @@ VoltDB还使用存储过程进行复制:但不是将事务的写入结果从
|
||||
|
||||
### 两阶段锁定(2PL)
|
||||
|
||||
大约30年来,在数据库中只有一种广泛使用的序列化算法:**两阶段锁定(2PL,two-phase locking)**[^xi]
|
||||
大约30年来,在数据库中只有一种广泛使用的序列化算法:**两阶段锁定(2PL,two-phase locking)** [^xi]
|
||||
|
||||
[^xi]: 有时也称为**严格两阶段锁定(SS2PL, strict two-phas locking)**,以便和其他2PL变体区分。
|
||||
|
||||
@ -681,7 +681,7 @@ VoltDB还使用存储过程进行复制:但不是将事务的写入结果从
|
||||
|
||||
之前我们看到锁通常用于防止脏写(参阅“[没有脏写](没有脏写)”一节):如果两个事务同时尝试写入同一个对象,则锁可确保第二个写入必须等到第一个写入完成事务(中止或提交),然后才能继续。
|
||||
|
||||
两阶段锁定定类似,但使锁的要求更强。只要没有写入,就允许多个事务同时读取同一个对象。但对象只要有写入(修改或删除),就需要**独占访问(exclusive access)**权限:
|
||||
两阶段锁定定类似,但使锁的要求更强。只要没有写入,就允许多个事务同时读取同一个对象。但对象只要有写入(修改或删除),就需要**独占访问(exclusive access)** 权限:
|
||||
|
||||
- 如果事务A读取了一个对象,并且事务B想要写入该对象,那么B必须等到A提交或中止才能继续。 (这确保B不能在A底下意外地改变对象。)
|
||||
- 如果事务A写入了一个对象,并且事务B想要读取该对象,则B必须等到A提交或中止才能继续。 (像[图7-1]()那样读取旧版本的对象在2PL下是不可接受的。)
|
||||
@ -758,17 +758,17 @@ WHERE room_id = 123 AND
|
||||
|
||||
本章描绘了数据库中并发控制的黯淡画面。一方面,我们实现了性能不好(2PL)或者扩展性不好(串行执行)的可序列化隔离级别。另一方面,我们有性能良好的弱隔离级别,但容易出现各种竞争条件(丢失更新,写入偏差,幻读等)。序列化的隔离级别和高性能是从根本上相互矛盾的吗?
|
||||
|
||||
也许不是:一个称为**可序列化快照隔离(SSI, serializable snapshot isolation)**的算法是非常有前途的。它提供了完整的可序列化隔离级别,但与快照隔离相比只有只有很小的性能损失。 SSI是相当新的:它在2008年首次被描述【40】,并且是Michael Cahill的博士论文【51】的主题。
|
||||
也许不是:一个称为**可序列化快照隔离(SSI, serializable snapshot isolation)** 的算法是非常有前途的。它提供了完整的可序列化隔离级别,但与快照隔离相比只有只有很小的性能损失。 SSI是相当新的:它在2008年首次被描述【40】,并且是Michael Cahill的博士论文【51】的主题。
|
||||
|
||||
今天,SSI既用于单节点数据库(PostgreSQL9.1 以后的可序列化隔离级别)和分布式数据库(FoundationDB使用类似的算法)。由于SSI与其他并发控制机制相比还很年轻,还处于在实践中证明自己表现的阶段。但它有可能因为足够快而在未来成为新的默认选项。
|
||||
|
||||
#### 悲观与乐观的并发控制
|
||||
|
||||
两阶段锁是一种所谓的**悲观并发控制机制(pessimistic)**:它是基于这样的原则:如果有事情可能出错(如另一个事务所持有的锁所表示的),最好等到情况安全后再做任何事情。这就像互斥,用于保护多线程编程中的数据结构。
|
||||
两阶段锁是一种所谓的**悲观并发控制机制(pessimistic)** :它是基于这样的原则:如果有事情可能出错(如另一个事务所持有的锁所表示的),最好等到情况安全后再做任何事情。这就像互斥,用于保护多线程编程中的数据结构。
|
||||
|
||||
从某种意义上说,串行执行可以称为悲观到了极致:在事务持续期间,每个事务对整个数据库(或数据库的一个分区)具有排它锁,作为对悲观的补偿,我们让每笔事务执行得非常快,所以只需要短时间持有“锁”。
|
||||
|
||||
相比之下,**序列化快照隔离**是一种**乐观(optimistic)**的并发控制技术。在这种情况下,乐观意味着,如果存在潜在的危险也不阻止事务,而是继续执行事务,希望一切都会好起来。当一个事务想要提交时,数据库检查是否有什么不好的事情发生(即隔离是否被违反);如果是的话,事务将被中止,并且必须重试。只有可序列化的事务才被允许提交。
|
||||
相比之下,**序列化快照隔离**是一种**乐观(optimistic)** 的并发控制技术。在这种情况下,乐观意味着,如果存在潜在的危险也不阻止事务,而是继续执行事务,希望一切都会好起来。当一个事务想要提交时,数据库检查是否有什么不好的事情发生(即隔离是否被违反);如果是的话,事务将被中止,并且必须重试。只有可序列化的事务才被允许提交。
|
||||
|
||||
乐观并发控制是一个古老的想法【52】,其优点和缺点已经争论了很长时间【53】。如果存在很多**争用(contention)**(很多事务试图访问相同的对象),则表现不佳,因为这会导致很大一部分事务需要中止。如果系统已经接近最大吞吐量,来自重试事务的额外负载可能会使性能变差。
|
||||
|
||||
@ -780,7 +780,7 @@ WHERE room_id = 123 AND
|
||||
|
||||
先前讨论了快照隔离中的写入偏差(参阅“[写入偏差和幻像](#写入偏差与幻读)”)时,我们观察到一个循环模式:事务从数据库读取一些数据,检查查询的结果,并根据它看到的结果决定采取一些操作(写入数据库)。但是,在快照隔离的情况下,原始查询的结果在事务提交时可能不再是最新的,因为数据可能在同一时间被修改。
|
||||
|
||||
换句话说,事务基于一个**前提(premise)**采取行动(事务开始时候的事实,例如:“目前有两名医生正在值班”)。之后当事务要提交时,原始数据可能已经改变——前提可能不再成立。
|
||||
换句话说,事务基于一个**前提(premise)** 采取行动(事务开始时候的事实,例如:“目前有两名医生正在值班”)。之后当事务要提交时,原始数据可能已经改变——前提可能不再成立。
|
||||
|
||||
当应用程序进行查询时(例如,“当前有多少医生正在值班?”),数据库不知道应用逻辑如何使用该查询结果。在这种情况下为了安全,数据库需要假设任何对该结果集的变更都可能会使该事务中的写入变得无效。 换而言之,事务中的查询与写入可能存在因果依赖。为了提供可序列化的隔离级别,如果事务在过时的前提下执行操作,数据库必须能检测到这种情况,并中止事务。
|
||||
|
||||
@ -851,7 +851,7 @@ WHERE room_id = 123 AND
|
||||
|
||||
***读取偏差(不可重复读)***
|
||||
|
||||
在同一个事务中,客户端在不同的时间点会看见数据库的不同状态。**快照隔离**经常用于解决这个问题,它允许事务从一个特定时间点的一致性快照中读取数据。快照隔离通常使用**多版本并发控制(MVCC)**来实现。
|
||||
在同一个事务中,客户端在不同的时间点会看见数据库的不同状态。**快照隔离**经常用于解决这个问题,它允许事务从一个特定时间点的一致性快照中读取数据。快照隔离通常使用**多版本并发控制(MVCC)** 来实现。
|
||||
|
||||
***更新丢失***
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user