可串行化
在开始之前,我们先约定后文要用的符号:
| 符号 | 含义 |
|---|---|
| T1 | 表示事务 T1 |
| R(x) | 读取 x 的值 |
| W(x) | 修改 x 的值(怎么改的别管) |
| W(x)=(x+2) | 将 x 的值设置为 x+2 |
事务调度
先来介绍一下什么是事务调度。假设现在有两个事务,如下:
T1: R(x) -> W(x) -> W(z) -> R(y)
T2: R(x) -> R(y)
所谓调度就是将事务中的操作组成一个有序序列,例如,下面是对 T1 和 T2 的一个调度:
| T1 | T2 |
|---|---|
| R(x) | |
| W(x) | |
| W(z) | |
| R(y) | |
| R(x) | |
| R(y) |
下面也是一种调度:
| T1 | T2 |
|---|---|
| R(x) | |
| R(x) | |
| W(x) | |
| W(z) | |
| R(y) | |
| R(y) |
基本原则
调度一个非常基本的原则,就是对于每个事务而言,事务内部的操作的顺序必须保持不变。例如,下面就不是一个有效调度:
| T1 | T2 |
|---|---|
| R(y) | |
| R(x) | |
| W(x) | |
| W(z) | |
| R(x) | |
| R(y) |
因为 T1 的操作顺序发生了改变,T1 应该是 R(x) -> W(x) -> W(z) -> R(y),这里却变成了 R(y) -> R(x) -> W(x) -> W(z)
FIFO (先进先出)原则
提前说明:这一部分我用了一些还没有介绍过的概念,这些概念会在后文中说明。如果你不了解的话请不要过于深究,读了后文后在回过头来看即可。
显然仅从调度本身的定义考虑,我们可设计出非常多的调度方案,但是在设计调度方案的时候还应当遵循一个称为 FIFO(First In,First Out) 的原则。所谓的 FIFO 原则其实非常简单,就是指“先开始的先执行”,更具体来说,它有如下体现:
(1)先开始的事务先提交
考虑以下两个事务:
T1: R(x) -> R(y)
T2: R(x) -> R(x)
我们假设如果事务 T1 开始在 T2 之前,那么其串行调度(后面会介绍)应该也是 T1 先执行,T2 后执行,如下:
| T1 | T2 |
|---|---|
| R(x) | |
| R(y) | |
| R(x) | |
| R(x) |
而下面的串行调度就不符合该原则,因为 T1 反而后执行了(虽然这个例子中谁先执行都不影响结果)
| T1 | T2 |
|---|---|
| R(x) | |
| R(x) | |
| R(x) | |
| R(y) |
(2)相邻的冲突操作的执行顺序应该与时间戳顺序一致
对于如下事务(此处用表格来表示)
T1: R(x) -> W(z)
T2: W(x)
T1 的 R(x)与 T2 的 W(x)是一对冲突操作,假设 T1 的 R(x)的时间戳比 T2 的 W(x)的时间戳更早,那么你的调度 S1 应该长这样:
| T1 | T2 |
|---|---|
| R(x) | |
| W(x) | |
| R(y) |
需要注意的是,我们只要求冲突操作的时间戳保持顺序,对于非冲突操作则允许互换。也就是说下面也是一个符合原则的调度 S2,因为 R(y)不和 T2 的任何操作冲突,所以可以和 T2 的 W(X)交换顺序:
| T1 | T2 |
|---|---|
| R(x) | |
| R(y) | |
| W(x) |
值得一提的是其实这里的调度 S1 是一并发调度,而调度 S2 则是一个串行调度。我们可以通过简单分析发现 S1 和 S2 具有等价性(每个操作的结果都相同),那么其实 S1 便是一个可串行化调度。
串行调度与可串行调度
先来了解一个概念:串行调度。所谓串行调度就是事务之间没有交错执行,比如,下面是一个串行调度 S1:
| T1 | T2 |
|---|---|
| R(x) | |
| W(x)=(x+2) | |
| W(y)=(y+5) | |
| R(y) |
而下面就是个非串行调度(也叫并发调度) S2:
| T1 | T2 |
|---|---|
| R(x) | |
| W(y)=(y+5) | |
| W(x)=(x+2) | |
| R(y) |
而可串行化调度指的就是:一个调度的结果与某一个串行调度的结果等价。所谓等价用比较直白的话来说就是包含了如下要求:
- 每一次 R 读到的数据都要是相同的
- 事务执行完成后的相关数据的最终值要相等
上述例子中的 S2 就是一种可串行化调度,它的执行结果与串行调度 S1 就是等价的。可以尝试自己分析一下。
而将非串行化调度转化为串行化调度就是串行化,上述的 S1 就是 S2 的串行化结果。
串行化问题
我们可以(十分突兀地引入)提出两个极具讨论价值的问题:
- 可串行化有没有什么分类?(能想到这个问题的是有点牛批的)
- 如何设计一个良好的可串行化调度?(所谓良好就是满足 FIFO 原则)
分类
我们先来看一个不可串行化的调度 S
| T1 | T2 |
|---|---|
| R(x) | |
| W(x)=(x+2) | |
| W(x)=(x*3) | |
| R(x) |
尝试下面两个串行调度
| T1 | T2 |
|---|---|
| R(x) | |
| W(x)=(x*3) | |
| W(x)=(x+2) | |
| R(x) |
| T1 | T2 |
|---|---|
| W(x)=(x+2) | |
| R(x) | |
| R(x) | |
| W(x)=(x*3) |
经过分析(由于分析较为啰嗦,此处不写出过程,分析工作请自行完成;我太懒了,不想写),可以发现这两个串行调度都不与 S 等价。(注:这里没有考虑 FIFO 原则)
再来看几个简单的可串行化的调度例子
(1)
| T1 |
|---|
| W(x) |
| R(x) |
What can I say?除了学数学的那些人可能会对这种问题的严格讨论感兴趣,我实在想不到单一事务在“可串行化”这个问题上有什么讨论价值,显然单个任务必然是可串行的。
(2)
| T1 | T2 |
|---|---|
| R(x) | R(x) |
| R(x) | R(x) |
毫无疑问 T1 和 T2 无论以什么顺序执行都不会影响每个操作的结果,因为它们都只有读取操作,x 的值在期间根本没有发生过任何改变。并发读取的一致性通常并不是什么问题。
(3)
| T1 | T2 |
|---|---|
| R(x) | |
| R(x) | |
| R(x) | |
| W(y)=(y+3) |
T2 发生了写操作,改变了 y 的值,但我们知道这两个事务依然是可串行化的,下面两种执行串行调度(现在暂时不考虑 FIFO 原则)
先 T1 后 T2
| T1 | T2 |
|---|---|
| R(x) | |
| W(y)=(y+3) | |
| R(x) | |
| R(x) |
先 T2 后 T1
| T1 | T2 |
|---|---|
| R(x) | |
| R(x) | |
| R(x) | |
| W(y)=(y+3) |
的结果都与原调度等价。我们知道可串行化的要求只是和某一个串行调度等价,而这甚至可以和两个串行调度等价,显然原调度是可串行化的。
但是为什么呢?很简单,T2 虽然有写操作,但是这个操作作用于 y 值,但 T1 和 T2 的其他的操作全都是作用于 x 值,并且均为读取操作。
现在我们简单归纳一下上述 3 个可串行化的例子,反过来整理一下哪些情况下一个调度可能无法串行化:
- 该调度中至少存在来自两个事务的操作
- 两个事务总共至少要有一次写入操作
- 两个事务各自至少有一个操作在同一个数据项上执行
而在以上三个条件的基础上,有两种重要的可串行化分类
1.冲突可串行化
所谓冲突就是两个事务中存在冲突操作,典型的冲突操作就是一个事务有读取 x 的操作,另一个事务有写入 x 的操作。下面是典型的冲突操作:
- $\exist R(x) \in T1 \space 且 \space \exist W(x) \in T2$
- $\exist W(x) \in T1 \space 且 \space \exist W(x) \in T2$
人话就是:在一个调度中,一个事务 T1 中有写 x 的操作,另一个事务 T2 中也对 x 有读或写的操作。至于同时读,随便吧。
当然,事务有冲突不代表不可串行化。那么现在我们知道了如何判断两个事务是否存在冲突,接下来就该分析一下两个有冲突的事务能否串行化了,先来看下面的例子
| T1 | T2 |
|---|---|
| R(x) | |
| R(x) | |
| R(y) | |
| W(x) |
我们能否把这个调度转化为串行调度呢?如果可以,我们就称其冲突可串行化。
那么转化的规则是什么呢?很简单,就是不断交换两个事务中相邻的不冲突的操作,直到可以形成一个串行调度。
具体步骤如下:
(1)交换 T1 的 R(x)与 T2 的 R(x)
| T1 | T2 |
|---|---|
| R(x) | |
| R(x) | |
| R(y) | |
| W(x) |
(2)交换 T1 的 R(x)与 T2 的 R(y)
| T1 | T2 |
|---|---|
| R(x) | |
| R(y) | |
| R(x) | |
| W(x) |
此时变形成了一个串行调度,所以该调度是冲突可串行化的。
与之相对的,我们来看一个不满足冲突可串行化的例子:
| T1 | T2 |
|---|---|
| W(x) | |
| R(x) | |
| W(x) |
显然在这个调度中无法通过任何交换得到一个串行调度,所以它不可以冲突串行化。
但是这里必须要提一点,一个调度不满足冲突可串行化不代表着该调度无法串行化,冲突可串行化只是可串行化的充分条件而不是必要条件,实际上冲突可串行化还是一个很强的条件;接下来要说的视图可串行化也是可串行化的充分不必要条件。
2.视图(view)可串行化
和冲突可串行化相比,视图可串行条件更弱,它不考虑是否能通过交换非冲突操作得到一个串行调度,它只考虑一点,那就是该调度是否等价于连续调度。
这么说有点抽象,来看一个实际例子。所谓的视图可串行化,就是要判断形如如下的调度:
| T1 | T2 |
|---|---|
| R(x) | |
| W(x) | |
| W(y) | |
| R(y) |
是否视图等价于如下连续调度:
| T1 | T2 |
|---|---|
| R(x) | |
| W(y) | |
| W(x) | |
| R(y) |
所谓视图等价需要满足以下两个条件:
- 每个 R 读取到的值是相同的
- 被操作的数据项最终值是相同的
说实话这里要分析就太啰嗦了,我太懒了,不想写。请尝试自己分析一下吧,答案是等价的。哦对了,我知道你在想什么,但别用冲突串行化的方法分析,请逐个分析两个调度中每个 R 操作读取到的值与调度结束后 x 和 y 的最终值。
不要因此认为冲突可串行与视图可串行等价,冲突可串行化的一定视图可串行化,但反过来并不成立。下面就是满足视图可串行化但不满足冲突可串行化的例子,假设 x 初值为 1,y 初值为 2:
| T1 | T2 | T3 |
|---|---|---|
| R(x) | ||
| W(x) | ||
| W(x) | ||
| W(x)=(x=114514) |
该调度显然不满足冲突可串行化,但其等价如下串行调度
| T1 | T2 | T3 |
|---|---|---|
| R(x) | ||
| W(x) | ||
| W(x) | ||
| W(x)=(x=114514) |
首先整个调度中,R 读到的值显然都是一致的,均为 1;再来看最终结果,可以发现两个调度中, x 的最终值都都等于 T3 的 W(x) 操作后的值,即 114514。
我知道你想说什么,但如果你注意力足够集中,或许能发现如果有一个调度满足视图可串行化但不满足冲突可串行化,那么这个调度中一定发生过至少一次盲目写(即写入的值不依赖于任何值)。这里的盲目写就是 W(x)=(x=114514),其写入值的时候无论 x 之前的值是多少,都直接修改为了 114514。
3.Ω 可串行化
你的笔记上有以一句话是这样的:
可串行化:两个序列 T1,T2,无论两个序列按何种顺序执行得到的结果都一致
其实我也不知道这个规则叫什么,而且这个规则其实是一个非常强的规则,我就简单粗暴叫它 Ω 可串行化了。它和冲突可串行化、视图可串行化以及可串行化的关系如下:
$$ Ω 可串行化 \subset 冲突可串行化 \subset 视图可串行化 \subset 可串行化 $$
作为反例,一个不满足 Ω 可串行化的调度如下:
| T1 | T2 |
|---|---|
| W(x)=(x+2) | W(x)=(x*3) |
我们来根据定义说明一下为什么两者不满足 Ω 可串行化。假设 x 初始值为 2,如果我们按如下串行调度执行
| T1 | T2 |
|---|---|
| W(x)=(x+2) | |
| W(x)=(x*3) |
则最终可得 x=12
而如果按如下串行调度执行
| T1 | T2 |
|---|---|
| W(x)=(x*3) | |
| W(x)=(x+2) |
则最终可得 x=8
显然两者不同的执行顺序会带来不同的结果,不满足我们 Ω 可串行化的条件。
与之相对的,下面是一个 Ω 可串行化的例子:
| T1 | T2 |
|---|---|
| R(x) | |
| R(x) | |
| R(x) | |
| W(y)=(y+3) |
这个例子前文其实已经看过了,就不再分析了。
设计一个可串行调度
综合前文,对于两个事务,如果我们要为其设计一个可串行化的调度,应当满足以下要求:
- 单个事务之间的操作顺序不能发生改变
- 调度应该满足 FIFO 原则
- 调度至少满足视图可串行化,通常建议以满足冲突可串行化为准。
举例来说,现在我们考虑如下两个事务(依然用表格表示)
| T1 | T2 | |
|---|---|---|
| 1 | W(x) | |
| 2 | R(x) | W(y) |
| 3 | W(x) | |
| 4 | W(z) | R(x) |
首先初步排个调度:
| T1 | T2 | |
|---|---|---|
| 1 | W(x) | |
| 2 | W(y) | |
| 3 | R(x) | |
| 4 | W(x) | |
| 5 | R(x) | |
| 6 | W(z) |
这个调度满足 FIFO 原则吗?满足。但是它可以串行化吗?我们试着检查一下它能否满足冲突可串行化,虽然实际上有一些更高级的算法来检查一个调度是否满足冲突可串行化,但在这里我们就简单手动交换一下相邻的非冲突操作来看看能否形成一个串行调度。
首先,将 T1 第 3 行的 R(x)与 T2 第 2 行的 W(y)交换:
| T1 | T2 | |
|---|---|---|
| 1 | W(x) | |
| 2 | R(x) | |
| 3 | W(y) | |
| 4 | W(x) | |
| 5 | R(x) | |
| 6 | W(z) |
接下来,通过逐层交换,把 T1 第 6 行的 W(z)交换到第 5 行,第 4 行,最终到第 3 行:
| T1 | T2 | |
|---|---|---|
| 1 | W(x) | |
| 2 | R(x) | |
| 3 | W(z) | |
| 4 | W(y) | |
| 5 | W(x) | |
| 6 | R(x) |
形成了一个串行调度,结论:该调度既满足 FIFO 原则,又满足冲突可串行化。
有趣的问题:反串行化调度
现在我们来看一个有趣的问题,虽然我也不知道有没有意义。那就是,给定一个串行调度,能否将其转变为一个并发调度?
假设串行调度如下:
| T1 |
|---|
| R(z) |
| W(x)=(y+2) |
| W(z)=(x*3) |
| R(x) |
| R(y) |
我们先简单将其拆分为两个事务(警告:这个问题只是讨论着玩的,现实中千万别说什么“拆分事务”这种话)
| T1 | T2 |
|---|---|
| R(z) | |
| W(x)=(y+2) | |
| W(z)=(x*3) | |
| R(x) | |
| R(y) |
在尝试之前,我们先来想想这些操作之间有没有什么顺序要求。毕竟,如果每个操作都有顺序要求,那么我们显然是没法将其转化为并发调度的。
我们可以很容易得出一个重要的顺序原则:如果一个操作读取了某个值 x,那么这个操作之前所有有关写入 x 的操作都必须在它之前完成,否则它读取到的值会不正确;另一方面,这个操作后面那些写入 x 的操作也必须在它之后完成。
这是显然的,先 W(x)后 R(x)显然不等价于先 R(x)后 W(x)。另外请注意,读取并不止 R(x),形如 W(x)=(y+2)也包含了读取操作(读取了 y 的值)。
现在我们观察一下这个串行调度,可以发现如下约束:
1:R(z)要保证在 W(z)=(x*3) 之前完成
2:W(z)=(x*3) 要在 W(x)=(y+2) 之后完成
3:R(x)要保证在 W(x)=(y+2) 之后完成
2:R(y)要什么时候读都行
3:...
总之,根据以上分析,我们可以对拆分后的事务做出如下重排序:
| T1 | T2 |
|---|---|
| W(x)=(y+2) | |
| R(x) | |
| R(z) | |
| R(y) | |
| W(z)=(x*3) |
这样我们就把一个串行调度变成了一个并发调度,当然你也可以排出其他顺序,只要满足“重要顺序原则”即可。

浙公网安备 33010602011771号