1、写在前面
1.1 为什么要并发控制
如果事务在并发执行时,来自各个并发事务的所有指令的执行控制都是由操作系统负责,那么许多调度都是可能的。这样,很可能会导致数据库处于不一致的状态。所以,必须保证数据库执行的任何调度都能是数据库保持一致状态,这是数据库中并发控制(concurrency-control)模块的功能。
具体地说,数据库的并发控制模块就是为用户提交的多个事务产生满足需求的调度。
1.2 并发控制的相关内容
1.2.1 内容列表
为了理解这一过程,我们需要了解:
- 并发中有关调度的相关基本概念
- 串行化及其判定
- 可恢复调度与无级联调度
- 事务的隔离级别等相关信息。
1.2.2 内容概要
基本概念部分,略过不表。
串行化及其判定部分大致介绍了如何判定事务在并发执行时是否和先后顺序执行时效果一致。明确了这部分,DBMS在为事务选择并发调度时,才知道最优解是什么(也就是,执行效果和先后执行相同)。
可恢复调度和无级联调度部分是在从故障恢复的角度讲述,如果一个并行执行的调度中间发生故障,为了保证事务的原子性,必须进行回滚恢复,然而,有时恢复的代价很大,有时甚至无法恢复。这部分对这些情况分别介绍。
事务的隔离级别部分讲述,在事务并发的过程中,如果要保证任何时刻绝对的数据正确,代价是很高的。比如,好多时候就无法实现并发,只能是串行执行。在一些联机的场景中,这是不能接受的。隔离级别就是为了兼顾效率的产物,通过允许不同程度地允许,并发过程中数据的暂时不一致,来换取更好的执行效率。
2、几个基本概念
- 调度(schedule)
事务在并发执行时,各个事务中的不同指令的先后执行顺序称为调度。比如事务T1由两条指令a和b组成,事务T2由c和d组成。那么,这两个事务在并发执行时abcd、acbd等的这些执行顺序都称之为调度。 - 串行的(serial)
如果在一个调度中,属于同一个事务的指令紧挨在一起,我们就称这个调度是串行的。上面的例子中,T1和T2的串行调度有两种,分别是abcd和cdab。对于n个事务组成的事务组,共有n!个不同的串行调度。 - 可串行化的(serializable)
如果一个调度等价于一个串行调度,那么就称该调度是可串行化的。显然,串行调度是可串行化的。
3、调度的可串行化
3.1 串行化与冲突可串行化
串行调度是可串行化的,但是,如果许多事务的指令交错执行,则很难确定一个调度是否是可串行化的。事务就是程序,要确定一个事务有哪些操作,多个事务的不同操作如何相互作用,是非常困难的。
因此,这里我们不会考虑一个事务可以对一个数据项执行的所有不同类型的操作,而只考虑两种操作:read和write。我们假设,在数据Q上的read(Q)和write(Q)之间,事务可以对驻留在事务局部缓冲区中Q的拷贝执行任意操作序列。按这种模式,从调度的角度来说,事务唯一重要的操作就是read和write。
假设I和J是不同事务在相同数据项上的操作,那么当它们全是read时,它们的次序无关紧要。但是,当其中至少有一个书write时,它们的顺序将直接影响最终事务的执行结果,这时我们说I和J是冲突(conflict)的。
如果调度S经过一系列非冲突指令次序交换转换成S',我们称S和S'是冲突等价(conflict equivalent)的。
可以理解,不是所有的串行调度之间都是冲突等价的。
如果一个调度与串行调度冲突等价,则称该调度是冲突可串行化(conflict serializable)的。
3.2 冲突可串行化的判定
这里给出一个简单有效的方法,来确定一个调度是否冲突可串行化。假设S是一个调度,我们由S构造一个有向图,称为优先图(precedence graph)。该图由定义为G=(V,E),其中V是顶点集,E是边集,顶点集由所有参与调度的事务组成。如果事务Ti和Tj满足下列三个条件之一,优先图中就存在边Ti->Tj:
- 在Tj执行read(Q)之前,Ti执行write(Q)。
- 在Tj执行write(Q)之前,Ti执行read(Q)。
- 在Tj执行write(Q)之前,Ti执行write(Q)。
这里的意思是,事务中冲突的操作决定了事务的执行顺序。所以,如果优先图中存在边Ti->Tj,则在任何等价于S的串行调度S’中,Ti必出现在Tj之前。
这样,如果调度S的优先图中有环,则调度S是非冲突可串行化的,如果优先图中无环,则调度S是冲突可串行化的。
串行化顺序(serializability order)可通过拓扑排序(topological sorting,用于计算与优先图的偏序相一致的线形顺序)得到。一般而言,通过拓扑排序可以获得多个线形顺序。
因此,要判断冲突可串行化,需要构造优先图并调用一个环检测算法。基于深度优先的环检测算法需要n^2数量级的运算,其中n是优先图中的定点数(即事务数)。
3.3 冲突等价的局限性
有可能存在两个调度,它们产生的结果相同,但它们不是冲突等价的。比如下面的例子:
利用前面提到的优先图判定方法,上图的调度S并不与串行调度<T1, T2>等价。然而,它们的执行结果却相同。
这个例子可以看出,调度等价的定义实际上是比冲突等价更为宽松,也就是说存在不是冲突等价的两个等价调度。
对于计算机来说,要判定调度S与串行调度<T1, T2>产生的结果相同,必须分析T1和T2所进行的计算,而不只是分析read和write操作。上面的例子比较简单,由于从数学的角度,递增和递减是可以交换的,导致两个调度等价。实际中,一个事务可能会表示为一条复杂的SQL语句,或一个有JDBC调用的Java程序等,这种判定的计算代价很大。
除此之外,也存在一些别的纯粹基于read和write操作的调度等价定义,比如视图等价,其中有视图可串行化的概念。这里暂且不做介绍。
4、事务的隔离性和原子性
不管是什么原因,如果事务Ti失败了,我们必须撤销该事务的影响以确保其原子性。在允许并发执行的系统中,原子性要求依赖于Ti的任何事务Tj(即Tj读取了Ti写的数据)也中止。为了确保这一点我们需要对系统所允许的调度类型做一些限制。
4.1 可恢复调度
如下所示的调度,事物T2只执行一条指令:read(A)。我们称之为部分调度(partial schedule)。因为T1中没有包括commit或abort操作。注意T2执行read(A)指令后立即提交。因此T2提交时T1仍处于活跃状态。现假定T1在提交前发生了故障。T2已经读取了T1写入的数据A的值(我们说T2依赖于T1)。因此,我们必须终止T2以保证事务的原子性。但T2已经提交,不能再中止。这样就出现了T1发生故障之后不能正确恢复的情形。
上面例子中的调度是一个不可恢复调度的例子。一个可恢复调度(recoverable schedule)应满足:对于每对事务Ti和Tj,如果Tj读取了之前由Ti所写的数据项,则Ti应该先于Tj提交。上面的例子如果是可恢复的,那么T2应该推迟至T1提交之后再提交。
4.2 无级联调度
即使一个调度是可恢复的,要从事务Ti的故障中正确恢复,可能需要回滚若干事务。当其它事务读取了Ti写入的数据项时就会发生这种情况。下面调度中,如果T1发生故障,回滚。由于T2读取了T1写入的数据A,T2必须回滚。同理,T3也必须回滚。这种因单个事务故障导致一系列事务回滚的现象称为级联回滚(cascading rollback)。
级联回滚导致大量的撤销工作,这是我们不希望的。所以要对调度进行限制,避免级联回滚发生,这样的调度称为无级联调度。规范地说,无级联调度(cascadeless schedule)必须满足:对于事务Ti和Tj,如果Tj读取了先前由Ti所写的数据项,则Ti必须在Tj这一读操作之前提交。
容易理解,一个无级联调度也是可恢复调度。
5、事务的隔离级别
5.1 隔离级别定义和解释
- 读未提交(read uncommitted)
这是最低的隔离级别。意思是,事务在并发时,允许一个事务读取另一个事务已经修改但还未提交的数据。这种情况下,会导致脏读。脏读针对的是更新操作。比如,事务T1更新了数据库中记录A的值,没有提交,T2读取了记录A,然后,T1回滚。这样,T2读取到的就是一个错误的数据。这种现象就叫脏读。 - 读提交(read committed)
事务在并发执行时,只允许读取其它事务已经提交的数据。
这样,可以解决数据的脏读问题,但是并不能保证可重复读。比如,事务T1中有两次对记录A的读取操作,在这两次读取操作之间,事务T2修改了记录A的值并提交。这样,T1两次读取到的值就会不同,这种现象成为不可重复读。 - 可重复读(repeatable read)
事务在并发执行时,只允许读取已经提交的数据,而且一个事务在两次读取一个数据项期间,其他事务不得更新该数据。这样,就保证了数据的可重复读。但是,也存在幻读的问题。
幻读,幻读针对的是插入操作。比如事务T1中选出数据库中符合条件的记录,然后,事务T2又向数据库中插入了一条数据,也符合T1的筛选条件,然后提交,这时,T1第二次查找符合条件的数据,就会发现结果集中多了一条记录。就好像出现了幻觉,所以称为幻读。 - 可串行化(serializable)
通常保证可串行化调度。但是,一些数据库对该隔离级别的实现,在某些情况下允许非串行化执行。
5.2 上述事务隔离级别的说明
从上到下,隔离级别依次提高。每个隔离级别的定义和解释中,说的都是该级别的最低要求。
所有的隔离级别都不允许脏写(dirty write),即如果一个数据项已经被另外一个未提交或者终止的事务写入,则不允许其它事务对该数据项进行写操作。
实现上,大多数数据库默认的事务隔离级别是Read committed,比如Sql Server , Oracle。Mysql的默认隔离级别是Repeatable read。
SQL中,可以显示设定事务的隔离级别。如可以通过语句set transaction isolation level serializable;
来显示将隔离级别设置为可串行化。另外,修改事务隔离级别必须作为事务的第一条语句执行。