事务的概念
构成单一逻辑工作单元的操作集合称为事务(transaction)
事务的 ACID 特性
原子性(atomicity)
同一个事务中的所有操作,要么全都执行,要么全都不执行,如果一个事务在执行过程中因为某些原因执行失败,那么已经执行的操作就要全部撤销,这个撤销的过程叫作回滚(rollback)
一致性(consistency)
如果数据库中的数据满足预设的全部规则,则称数据库中的数据是一致的
如果在执行事务之前,数据库中的数据是一致的,那么事务执行完毕之后,数据库中的数据仍然要是一致的,保证事务具有一致性是编写事务的程序员的职责
隔离性(isolation)
尽管多个事务可能并行执行,但对于任何一对事务 T 1 T_1 T1 和 T 2 T_2 T2 ,在 T 1 T_1 T1 看来,要么 T 2 T_2 T2 还没开始执行,要么 T 2 T_2 T2 已经执行完毕
严格保证隔离性可能会降低数据库系统的性能,因此,在一些数据库系统中可以对隔离性作出一些妥协
持久性(durability)
一个事务执行完成后,对数据库的改变就应该是永久的,即使之后发生了系统故障,在故障修复之后,事务对数据库的改变也不能丢失
事务的状态
事务有以下几种状态:
- 活动状态:一个事务开始执行时,事务就进入了活动状态,对于一个活动状态的事务,如果数据库系统判断其不能继续正常执行,事务就会进入失败状态
- 部分提交状态:当一个事务的操作全部执行完毕时,事务就从活动状态进入了部分提交状态,之所以不是提交状态,是因为此时有些应该被写入磁盘的数据还保留在数据库缓冲区中,进入部分提交状态的事务,还需要等待这些数据被真正写入磁盘,在写入这些数据时,事务仍然可能由于某些原因,从而进入失败状态
- 提交状态:当一个部分提交状态的事务,向磁盘中写入了足够的数据,以至于即使出现了故障,在故障修复后也能恢复事务对数据库的影响,那么事务就进入了提交状态
- 失败状态:活动状态和部分提交状态的事务,都可能因为某些原因进入失败状态,处于失败状态的事务等待回滚
- 中止状态:当一个处于失败状态的事务回滚之后,事务就进入了中止状态,此时数据库系统有两种选择,要么重启事务,要么杀死事务,若事务失败的原因是内部的逻辑错误,则只能选择杀死事务
事务不同状态之间的相互转换如下:
并行事务的调度
并行的原因
如果所有事务都串行地执行,那么事务的隔离性和一致性都能得到保证,当多个事务并行执行时,我们就需要为保证隔离性和一致性而进行额外的工作,但允许事务并行执行有很大的好处
在计算机系统中有许多计算资源,如果事务仅仅是串行地执行,那么在很多情况下,计算资源的利用率很低,当事务在 CPU 上计算时,I/O 资源就被闲置,当事务等待 I/O 时,CPU 资源又被浪费,此外,对于多核处理器,一个事务通常情况下只能使用处理器的一个核,那么其它的核就被浪费了
在多个事务并行执行时,计算机系统的资源利用率显著提高,这样我们虽然没有加快单个事务的执行速度,但吞吐量会大大提升
并行执行事务的原因本质上和操作系统中使用多道程序的原因是一样的
调度
每个事务都是由许多指令构成的,一组事务中指令执行的时间顺序就叫作关于这组事务的调度,显然,一组事务的调度需要包含这组事务的全部指令,并且同一个事务中的指令顺序必须保持
串行调度
对于同一组事务的不同调度,产生的结果可能不同,其中一些调度可能会破坏事务的隔离性和一致性
如果一个调度中同一个事务中的指令紧挨在一起,这个调度就是一个串行调度,我们已经知道,串行调度是不会破坏事务的隔离性和一致性的
冲突可串行化
如果一个调度产生的结果和某个串行调度产生的结果相同,那么我们认为这个调度和串行调度等价,对于一个调度,我们希望能判断它是否和某个串行调度等价,因为只有这样才能知道这个调度是否会破坏隔离性和一致性
但是,要想精确判断一个调度是否和某个串行调度等价,需要仔细分析每条指令的内容,这种分析通常难以实现且代价很大,不过有方法可以找出一部分和串行调度等价的调度
对于一个调度,我们只考虑它的 I/O 操作,当两条相邻的 I/O 操作 x x x 和 y y y 交换执行顺序时,考虑交换的影响(假设原本 x x x 在 y y y 之前执行):
- x x x 和 y y y 操作的数据不同时,交换 x , y x,y x,y 无任何影响
- x = r e a d ( d ) x=read(d) x=read(d) , y = r e a d ( d ) y=read(d) y=read(d) 时,交换 x , y x,y x,y 无任何影响
- x = r e a d ( d ) x=read(d) x=read(d) , y = w r i t e ( d ) y=write(d) y=write(d) 时,交换 x , y x,y x,y 影响 x x x 读取的结果
- x = w r i t e ( d ) x=write(d) x=write(d) , y = r e a d ( d ) y=read(d) y=read(d) 时,交换 x , y x,y x,y 影响 x x x 写入的内容
- x = w r i t e ( d ) x=write(d) x=write(d) , y = w r i t e ( d ) y=write(d) y=write(d) 时,交换 x , y x,y x,y 影响最终写入的内容
对于一个调度 S S S ,交换其相邻的 I/O 操作若干次,得到调度 S ′ S' S′ ,若对于每次交换,交换的两个操作针对的数据不同,或交换的两个操作都是读取操作,则我们称 S ′ S' S′ 和 S S S 冲突等价
如果一个调度和某个串行调度冲突等价,则我们称这个调度是冲突可串行化的,设冲突可串行化的调度组成的集合为 P P P ,所有和某个串行调度等价的调度组成的集合为 Q Q Q ,则 P P P 是 Q Q Q 的一个子集
虽然精确判断一个调度是否和某个串行调度等价很难,但有很好的方法判断一个调度是否是冲突可串行化的
对于一个调度 S S S ,可以将其按以下方法转化成一个有向图
调度中每个事务,就是有向图的一个点,如果满足这几种情况之一,就连一条事务 T 1 T_1 T1 指向事务 T 2 T_2 T2 的有向边
- 在 T 2 T_2 T2 执行 r e a d ( d ) read(d) read(d) 之前, T 1 T_1 T1 执行 w r i t e ( d ) write(d) write(d)
- 在 T 2 T_2 T2 执行 w r i t e ( d ) write(d) write(d) 之前, T 1 T_1 T1 执行 r e a d ( d ) read(d) read(d)
- 在 T 2 T_2 T2 执行 w r i t e ( d ) write(d) write(d) 之前, T 1 T_1 T1 执行 w r i t e ( d ) write(d) write(d)
如果在图上有一条 T 1 T_1 T1 指向 T 2 T_2 T2 的有向边,就说明在任何与 S S S 冲突等价的串行调度 S ′ S' S′ 中, T 1 T_1 T1 在 T 2 T_2 T2 之前执行
因此,如果由调度 S S S 转化成的有向图上有环,则 S S S 不是冲突可串行化的,如果没有环,则 S S S 是冲突可串行化的,在没有环的前提下,还可以对图上的点进行拓扑排序,从而得到一个和调度 S S S 等价的串行调度中执行各事务的顺序
事务的隔离性级别
如果数据库系统中产生的所有调度都是等价于串行调度的,那么事务的隔离性和一致性就得到了保障,但是数据库系统之所以选择并行地执行事务,就是为了提高吞吐量和资源利用率,而在有些情况下,如果要保证调度都等价于串行调度,那么事务在执行时就只能允许很小的并发度,也就是说对吞吐量和资源利用率的提高不大,这违背了我们的初衷
因此数据库系统通常提供了一些其它的方案,允许我们产生一些不等价于串行调度的调度,这导致了较弱的隔离性和一致性,但提高了事务执行时的并发度,在使用这些方案时,程序员需要进行一些额外的工作来保证数据库中的数据在事务执行完毕之后是一致的
SQL 标准规定了四种隔离性级别,按隔离性由强到弱依次为:可串行化(serializable)、可重复读(repeatable read)、已提交读(read committed)、未提交读(read uncommitted)
可串行化是指,保证产生的调度和串行调度等价
可重复读是指,一个事务只允许读其它事务已经提交的数据,并且在该事务两次读取同一个数据期间,其它事务不允许修改该数据
已提交读是指,一个事务只允许读其它事务已经提交的数据
未提交读是指,一个事务可以读其它未提交事务修改过的数据
一致性问题
并行执行事务时,如果隔离性级别不是可串行化,就可能会出现一些一致性问题,常见的一致性问题如下
脏写是指,一个事务修改了另一个未提交事务修改过的数据,SQL 标准规定的任何隔离性级别都不允许出现脏写
脏读是指,一个事务 T 1 T_1 T1 读取了另一个未提交事务 T 2 T_2 T2 修改过的数据,之后 T 2 T_2 T2 回滚,导致了 T 1 T_1 T1 读取到的数据无效,只有隔离性级别是未提交读时,才有可能出现脏读现象
不可重复读是指,一个事务先后两次读取同一个数据,但得到的值却不一样,这一般是由于两次读取之间,有另一个事务修改了该数据并提交,当隔离性级别为未提交读或已提交读时,可能会出现不可重复读现象
幻读是指,一个事务先后两次查找满足某种条件的数据,且两次查找条件一样,但后一次查找的结果集比前一次的结果集多出了一些数据,这一般是由于两次查找之间有另一个事务向被查找的关系中添加了数据并提交,当隔离性级别为未提交读、已提交读或可重复读时,都有可能出现幻读现象
其它调度规则
之前我们一直假设事务的执行不会出现故障,但事实上事务有可能因为某些原因而进行回滚,如果一个事务 T 1 T_1 T1 读取了另一个事务 T 2 T_2 T2 修改过的数据,则我们称 T 1 T_1 T1 依赖于 T 2 T_2 T2
在一个调度 S S S 中,如果存在一对事务 T 1 T_1 T1 和 T 2 T_2 T2 ,满足 T 1 T_1 T1 依赖于 T 2 T_2 T2 ,并且 T 1 T_1 T1 在 T 2 T_2 T2 提交之前提交,则我们称调度 S S S 是一个不可恢复调度,因为在 T 1 T_1 T1 提交之后, T 2 T_2 T2 存在回滚的可能,由于 T 1 T_1 T1 已经提交,无法再撤销,因此 T 1 T_1 T1 可能由于读取了已失效的数据从而产生了错误的结果
因此数据库系统一般需要保证所有调度都是可恢复调度,也就是说对于调度中任何一对事务 T 1 T_1 T1 和 T 2 T_2 T2 ,若 T 1 T_1 T1 读取了 T 2 T_2 T2 修改过的数据,则 T 1 T_1 T1 应该在 T 2 T_2 T2 提交之后才允许提交
通常情况下,只有隔离性级别为未提交读时,才需要注意数据库系统才需要注意产生的调度是否为可恢复调度,因为只有未提交读的级别下,一个事务才有可能读另一个未提交事务修改过的数据
即使一个调度是可恢复调度,依然会存在某些问题,当事务 T 1 T_1 T1 回滚时,由于另一个事务 T 2 T_2 T2 读取了 T 1 T_1 T1 修改过的数据,因此 T 2 T_2 T2 也要回滚,而 T 2 T_2 T2 回滚又可能造成更多事务的回滚,这种现象称为级联回滚,只有隔离性级别为未提交读时,才有可能出现级联回滚,为了避免级联回滚,可以将隔离性级别提高到已提交读及以上