mirror of
https://github.com/Vonng/ddia.git
synced 2024-12-06 15:20:12 +08:00
ch7 100%
This commit is contained in:
parent
44043e6790
commit
8b595b31bc
65
ddia/ch7.md
65
ddia/ch7.md
@ -665,19 +665,19 @@ VoltDB还使用存储过程进行复制:但不是将事务的写入结果从
|
||||
|
||||
[^x]: 如果事务需要访问不在内存中的数据,最好的解决方案可能是中止事务,异步地将数据提取到内存中,同时继续处理其他事务,然后在数据加载完毕时重新启动事务。这种方法被称为**反缓存(anti-caching)**,正如前面在第88页“将所有内容保存在内存”中所述。
|
||||
|
||||
### 两阶段锁(2PL)
|
||||
### 两阶段锁定(2PL)
|
||||
|
||||
大约30年来,在数据库中只有一种广泛使用的序列化算法:**两阶段锁(2PL,two-phase locking)**[^xi]
|
||||
大约30年来,在数据库中只有一种广泛使用的序列化算法:**两阶段锁定(2PL,two-phase locking)**[^xi]
|
||||
|
||||
[^xi]: 有时也称为严格两阶段锁(SS2PL, strict two-phas locking),以便和其他2PL变体区分。
|
||||
[^xi]: 有时也称为严格两阶段锁定(SS2PL, strict two-phas locking),以便和其他2PL变体区分。
|
||||
|
||||
> #### 2PL不是2PC
|
||||
>
|
||||
> 请注意,虽然两阶段锁(2PL)听起来非常类似于两阶段提交(2PC),但它们是完全不同的东西。我们将在第9章讨论2PC。
|
||||
> 请注意,虽然两阶段锁定(2PL)听起来非常类似于两阶段提交(2PC),但它们是完全不同的东西。我们将在第9章讨论2PC。
|
||||
|
||||
之前我们看到锁通常用于防止脏写(参阅“[无脏写]()”一节):如果两个事务同时尝试写入同一个对象,则锁可确保第二个写入必须等到第一个写入完成事务(中止或提交),然后才能继续。
|
||||
|
||||
两相锁定类似,但使锁的要求更强。只要没有写入,就允许多个事务同时读取同一个对象。但对象只要有写入(修改或删除),就需要**独占访问(exclusive access)**权限:
|
||||
两阶段锁定定类似,但使锁的要求更强。只要没有写入,就允许多个事务同时读取同一个对象。但对象只要有写入(修改或删除),就需要**独占访问(exclusive access)**权限:
|
||||
|
||||
- 如果事务A读取了一个对象,并且事务B想要写入该对象,那么B必须等到A提交或中止才能继续。 (这确保B不能在A底下意外地改变对象。)
|
||||
- 如果事务A写入了一个对象,并且事务B想要读取该对象,则B必须等到A提交或中止才能继续。 (像[图7-1]()那样读取旧版本的对象在2PL下是不可接受的。)
|
||||
@ -699,9 +699,9 @@ VoltDB还使用存储过程进行复制:但不是将事务的写入结果从
|
||||
|
||||
由于使用了这么多的锁,因此很可能会发生:事务A等待事务B释放它的锁,反之亦然。这种情况叫做**死锁(Deadlock)**。数据库会自动检测事务之间的死锁,并中止其中一个,以便另一个继续执行。被中止的事务需要由应用程序重试。
|
||||
|
||||
#### 二阶段锁定的性能
|
||||
#### 两阶段锁定的性能
|
||||
|
||||
二阶段锁定的巨大缺点,以及70年代以来没有被所有人使用的原因,是其性能问题。两阶段锁定下的事务吞吐量与查询响应时间要比弱隔离级别下要差得多。
|
||||
两阶段锁定的巨大缺点,以及70年代以来没有被所有人使用的原因,是其性能问题。两阶段锁定下的事务吞吐量与查询响应时间要比弱隔离级别下要差得多。
|
||||
|
||||
这一部分是由于获取和释放所有这些锁的开销,但更重要的是由于并发性的降低。按照设计,如果两个并发事务试图做任何可能导致竞争条件的事情,那么必须等待另一个完成。
|
||||
|
||||
@ -752,7 +752,7 @@ WHERE room_id = 123 AND
|
||||
|
||||
|
||||
|
||||
### 可串行快照隔离(SSI)
|
||||
### 序列化快照隔离(SSI)
|
||||
|
||||
本章描绘了数据库中并发控制的黯淡画面。一方面,我们实现了性能不好(2PL)或者扩展性不好(串行执行)的可序列化隔离级别。另一方面,我们有性能良好的弱隔离级别,但容易出现各种竞争条件(丢失更新,写入偏差,幻读等)。序列化的隔离级别和高性能是从根本上相互矛盾的吗?
|
||||
|
||||
@ -762,33 +762,34 @@ WHERE room_id = 123 AND
|
||||
|
||||
#### 悲观与乐观的并发控制
|
||||
|
||||
两阶段锁定是一种所谓的**悲观并发控制机制(pessimistic)**:它是基于如果有什么可能出错(如另一个事务所持有的锁所表示的)的原则,最好等到情况再次安全做任何事情。这就像互斥,用于保护多线程编程中的数据结构。
|
||||
两阶段锁是一种所谓的**悲观并发控制机制(pessimistic)**:它是基于这样的原则:如果有事情可能出错(如另一个事务所持有的锁所表示的),最好等到情况安全后再做任何事情。这就像互斥,用于保护多线程编程中的数据结构。
|
||||
|
||||
从某种意义上说,串行执行可以称为悲观到了极致:在事务持续期间,每个事务对整个数据库(或数据库的一个分区)具有独占锁定,这基本相当。我们通过使每笔事务执行得非常快来弥补悲观情绪,所以只需要短时间保持“锁定”。
|
||||
从某种意义上说,串行执行可以称为悲观到了极致:在事务持续期间,每个事务对整个数据库(或数据库的一个分区)具有排它锁,作为对悲观的补偿,我们让每笔事务执行得非常快,所以只需要短时间持有“锁”。
|
||||
|
||||
相比之下,可串行化的快照隔离是一种乐观的并发控制技术。在这种情况下乐观意味着,如果发生某种可能的危险,不要阻止事务,反而继续事务,希望一切都会好起来。当一个事务想要提交时,数据库检查是否有什么不好的事情发生(即隔离是否被违反);如果是的话,事务将被中止,并且必须重试。只有可序列化的事务才被允许提交。
|
||||
相比之下,**序列化快照隔离**是一种**乐观(optimistic)**的并发控制技术。在这种情况下,乐观意味着,如果存在潜在的危险也不阻止事务,而是继续执行事务,希望一切都会好起来。当一个事务想要提交时,数据库检查是否有什么不好的事情发生(即隔离是否被违反);如果是的话,事务将被中止,并且必须重试。只有可序列化的事务才被允许提交。
|
||||
|
||||
乐观并发控制是一个古老的想法[52],其优点和缺点已经争论了很长时间[53]。如果存在较高的意图(很多事务试图访问相同的对象),则表现不佳,因为这会导致很大一部分事务需要中止。如果系统已经接近最大吞吐量,来自重试事务的额外事务负载可能会使性能变差。
|
||||
乐观并发控制是一个古老的想法[52],其优点和缺点已经争论了很长时间[53]。如果存在很多**争用(contention)**(很多事务试图访问相同的对象),则表现不佳,因为这会导致很大一部分事务需要中止。如果系统已经接近最大吞吐量,来自重试事务的额外负载可能会使性能变差。
|
||||
|
||||
但是,如果有足够的备用容量,并且事务之间的争用不是太高,乐观的并发控制技术往往比悲观的要好。可交换的原子操作可以减少争用:例如,如果多个事务同时要增加一个计数器,那么应用增量的顺序(只要计数器不在同一个事务中读取)就无关紧要了,所以并发增量可以全部应用而不冲突。
|
||||
但是,如果有足够的备用容量,并且事务之间的争用不是太高,乐观的并发控制技术往往比悲观的要好。可交换的原子操作可以减少争用:例如,如果多个事务同时要增加一个计数器,那么应用增量的顺序(只要计数器不在同一个事务中读取)就无关紧要了,所以并发增量可以全部应用且无需冲突。
|
||||
|
||||
顾名思义,SSI基于快照隔离 - 也就是说,事务中的所有读取操作都是通过数据库的一致快照创建的(请参见第237页的“快照隔离和可重复读取”)。与早期的乐观并发控制技术相比,这是主要的区别。在快照隔离的基础上,SSI添加了一种算法来检测写入之间的序列化冲突,并确定要中止哪些事务。
|
||||
顾名思义,SSI基于快照隔离——也就是说,事务中的所有读取都是来自数据库的一致性快照(参见“[快照隔离和可重复读取]()”)。与早期的乐观并发控制技术相比这是主要的区别。在快照隔离的基础上,SSI添加了一种算法来检测写入之间的序列化冲突,并确定要中止哪些事务。
|
||||
|
||||
#### 基于过时前提的决定
|
||||
#### 基于过时前提的决策
|
||||
|
||||
当我们先前讨论了快照隔离中的写入歪斜(请参阅“写入歪斜和幻像”第221页)时,我们观察到一个循环模式:事务从数据库读取一些数据,检查查询的结果,并决定采取一些操作(写入数据库)根据它看到的结果。但是,在快照隔离的情况下,原始查询的结果在事务提交时可能不再是最新的,因为数据可能在同一时间被修改。
|
||||
换句话说,事务正在基于一个前提采取行动(事务一开始就是事实,例如“目前有两名医生正在通话”)。之后,当事务要提交时,原始数据可能已经改变 - 前提可能不再成立。
|
||||
先前讨论了快照隔离中的写入偏差(请参阅“写入歪斜和幻像”第221页)时,我们观察到一个循环模式:事务从数据库读取一些数据,检查查询的结果,并根据它看到的结果决定采取一些操作(写入数据库)。但是,在快照隔离的情况下,原始查询的结果在事务提交时可能不再是最新的,因为数据可能在同一时间被修改。
|
||||
|
||||
当应用程序进行查询时(例如,“当前有多少医生正在调用?”),数据库不知道应用程序逻辑如何使用该查询的结果。为了安全,数据在这种情况下,事务可能在一个过时的前提下采取了行动并放弃事务。
|
||||
换句话说,事务基于一个**前提(premise)**采取行动(事务开始时候的事实,例如:“目前有两名医生正在通话”)。之后当事务要提交时,原始数据可能已经改变——前提可能不再成立。
|
||||
|
||||
当应用程序进行查询时(例如,“当前有多少医生正在调用?”),数据库不知道应用逻辑如何使用该查询结果。在这种情况下为了安全,数据库需要假设任何对该结果集的变更都可能会使该事务中的写入变得无效。 换而言之,事务中的查询与写入可能存在因果依赖。为了提供可序列化的隔离级别,如果事务在过时的前提下执行操作,数据库必须能检测到这种情况,并中止事务。
|
||||
|
||||
数据库如何知道查询结果是否可能已经改变?有两种情况需要考虑:
|
||||
|
||||
- 检测陈旧的MVCC对象版本的读取(在读取之前发生未提交的写入)
|
||||
- 检测影响先前读取的写入(写入发生在读取之后)
|
||||
- 检测对旧MVCC对象版本的读取(读之前存在未提交的写入)
|
||||
- 检测影响先前读取的写入(读之后发生写入)
|
||||
|
||||
#### 检测陈旧的MVCC读取
|
||||
#### 检测旧MVCC读取
|
||||
|
||||
回想一下,快照隔离通常是通过多版本并发控制(MVCC;见图7-10)来实现的。当一个事务从MVCC数据库中一致的快照中读取时,它将忽略在拍摄快照时尚未提交的任何其他事务所做的写入。在图7-10中,事务43将Alice看作具有on_call = true,因为事务42(修改Alice的待命状态)未被提交。然而,在事务43想要提交时,事务42已经提交。这意味着从一致性快照读取时被忽略的写入已经生效,并且事务43的前提不再是真实的。
|
||||
回想一下,快照隔离通常是通过多版本并发控制(MVCC;见[图7-10]())来实现的。当一个事务从MVCC数据库中的一致快照读时,它将忽略取快照时尚未提交的任何其他事务所做的写入。在[图7-10]()中,事务43认为Alice的`on_call = true`,因为事务42(修改Alice的待命状态)未被提交。然而,在事务43想要提交时,事务42已经提交。这意味着在读一致性快照时被忽略的写入已经生效,事务43的前提不再为真。
|
||||
|
||||
![](img/fig7-10.png)
|
||||
|
||||
@ -796,7 +797,7 @@ WHERE room_id = 123 AND
|
||||
|
||||
为了防止这种异常,数据库需要跟踪一个事务由于MVCC可见性规则而忽略另一个事务的写入。当事务想要提交时,数据库检查是否有任何被忽略的写入现在已经被提交。如果是这样,事务必须中止。
|
||||
|
||||
为什么要等到提交?当检测到陈旧的读取时,为什么不立即中止事务43?那么,如果事务43是只读事务,则不需要中止,因为没有写歪斜的风险。当事务43进行读取时,数据库还不知道事务是否要稍后执行写操作。此外,事务42可能在事务43被提交的时候中止或者可能仍然未被提交,因此读取可能终究不是陈旧的。通过避免不必要的中止,SSI保留快照隔离对从一致快照中长时间运行的读取的支持。
|
||||
为什么要等到提交?当检测到陈旧的读取时,为什么不立即中止事务43?因为如果事务43是只读事务,则不需要中止,因为没有写入偏差的风险。当事务43进行读取时,数据库还不知道事务是否要稍后执行写操作。此外,事务42可能在事务43被提交的时候中止或者可能仍然未被提交,因此读取可能终究不是陈旧的。通过避免不必要的中止,SSI保留快照隔离对从一致快照中长时间运行的读取的支持。
|
||||
|
||||
#### 检测影响之前读取的写入
|
||||
|
||||
@ -806,25 +807,25 @@ WHERE room_id = 123 AND
|
||||
|
||||
**图7-11 在可序列化快照隔离中,检测一个事务何时修改另一个事务的读取。**
|
||||
|
||||
在两阶段锁定的上下文中,我们讨论了索引范围锁(请参阅“索引范围锁”,第259页),它允许数据库锁定与某个搜索查询匹配的所有行的访问权,例如WHERE shift_id = 1234。可以在这里使用类似的技术,除了SSI锁不会阻塞其他事务。
|
||||
在两阶段锁定的上下文中,我们讨论了[索引范围锁]()(请参阅“[索引范围锁]()”),它允许数据库锁定与某个搜索查询匹配的所有行的访问权,例如`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必须中止。
|
||||
|
||||
#### 可序列化的快照隔离的性能
|
||||
|
||||
与往常一样,许多工程细节会影响算法在实践中的效果。例如,一个权衡是跟踪事务的读取和写入的粒度。如果数据库非常详细地跟踪每个事务的活动,那么可以准确地确定哪些事务需要中止,但是记帐开销可能变得很重要。不太详细的跟踪速度更快,但可能会导致更多的事务被中止。
|
||||
与往常一样,许多工程细节会影响算法的实际表现。例如一个权衡是跟踪事务的读取和写入的**粒度(granularity)**。如果数据库详细地跟踪每个事务的活动(细粒度),那么可以准确地确定哪些事务需要中止,但是簿记开销可能变得很显著。简略的跟踪速度更快(粗粒度),但可能会导致更多不必要的事务中止。
|
||||
|
||||
在某些情况下,事务可以读取被另一个事务覆盖的信息:取决于发生了什么,有时可以证明执行的结果是可序列化的。 PostgreSQL使用这个理论来减少不必要的中止次数[11,41]。
|
||||
在某些情况下,事务可以读取被另一个事务覆盖的信息:这取决于发生了什么,有时可以证明执行结果无论如何都是可序列化的。 PostgreSQL使用这个理论来减少不必要的中止次数[11,41]。
|
||||
|
||||
与两阶段锁定相比,可序列化快照隔离的最大优点是一个事务不需要阻塞等待另一个事务所持有的锁。就像在快照隔离下一样,编写者不会阻止读者,反之亦然。这种设计原则使得查询延迟更可预测,变量更少。特别是,只读查询可以运行在一致的快照上,而不需要任何锁定,这对于读取繁重的工作负载非常有吸引力。
|
||||
与两阶段锁定相比,可序列化快照隔离的最大优点是一个事务不需要阻塞等待另一个事务所持有的锁。就像在快照隔离下一样,写不会阻塞读,反之亦然。这种设计原则使得查询延迟更可预测,变量更少。特别是,只读查询可以运行在一致的快照上,而不需要任何锁定,这对于读取繁重的工作负载非常有吸引力。
|
||||
|
||||
与串行执行相比,可串行化的快照隔离并不局限于单个CPU内核的吞吐量:FoundationDB将检测到的串行冲突分布在多台机器上,允许扩展到很高的吞吐量。即使数据可能跨多台机器进行分区,事务也可以在保证可串行化隔离的同时读写多个分区中的数据[54]。
|
||||
与串行执行相比,可序列化快照隔离并不局限于单个CPU核的吞吐量:FoundationDB将检测到的序列化冲突分布在多台机器上,允许扩展到很高的吞吐量。即使数据可能跨多台机器进行分区,事务也可以在保证可序列化隔离等级的同时读写多个分区中的数据[54]。
|
||||
|
||||
中止率显着影响SSI的整体表现。例如,长时间读取和写入数据的事务很可能会发生冲突并中止,因此SSI要求读写事务相当短(长时间运行的只读事务可能没有问题)。但是,SSI可能比两阶段锁定或串行执行更不敏感。
|
||||
中止率显着影响SSI的整体表现。例如,长时间读取和写入数据的事务很可能会发生冲突并中止,因此SSI要求同时读写的事务尽量短(长时间运行的只读事务可能没问题)。对于慢事务,SSI可能比两阶段锁定或串行执行更不敏感。
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user