如果事务在并发执行时,来自各个并发事务的所有指令的执行控制都是由操作系统负责,那么许多调度都是可能的。这样,很可能会导致数据库处于不一致的状态。所以,必须保证数据库执行的任何调度都能是数据库保持一致状态,这是数据库中并发控制(concurrency-control)模块的功能。
具体地说,数据库的并发控制模块就是为用户提交的多个事务产生满足需求的调度。
为了理解这一过程,我们需要了解:
- 并发中有关调度的相关基本概念
- 串行化及其判定
- 可恢复调度与无级联调度
- 事务的隔离级别等相关信息。
基本概念部分,略过不表。
串行化及其判定部分大致介绍了如何判定事务在并发执行时是否和先后顺序执行时效果一致。明确了这部分,DBMS在为事务选择并发调度时,才知道最优解是什么(也就是,执行效果和先后执行相同)。
可恢复调度和无级联调度部分是在从故障恢复的角度讲述,如果一个并行执行的调度中间发生故障,为了保证事务的原子性,必须进行回滚恢复,然而,有时恢复的代价很大,有时甚至无法恢复。这部分对这些情况分别介绍。
事务的隔离级别部分讲述,在事务并发的过程中,如果要保证任何时刻绝对的数据正确,代价是很高的。比如,好多时候就无法实现并发,只能是串行执行。在一些联机的场景中,这是不能接受的。隔离级别就是为了兼顾效率的产物,通过允许不同程度地允许,并发过程中数据的暂时不一致,来换取更好的执行效率。
串行调度是可串行化的,但是,如果许多事务的指令交错执行,则很难确定一个调度是否是可串行化的。事务就是程序,要确定一个事务有哪些操作,多个事务的不同操作如何相互作用,是非常困难的。
因此,这里我们不会考虑一个事务可以对一个数据项执行的所有不同类型的操作,而只考虑两种操作: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)的。
这里给出一个简单有效的方法,来确定一个调度是否冲突可串行化。假设S是一个调度,我们由S构造一个有向图,称为优先图(precedence graph)。该图由定义为G=(V,E),其中V是顶点集,E是边集,顶点集由所有参与调度的事务组成。如果事务Ti和Tj满足下列三个条件之一,优先图中就存在边Ti->Tj:
这里的意思是,事务中冲突的操作决定了事务的执行顺序。所以,如果优先图中存在边Ti->Tj,则在任何等价于S的串行调度S’中,Ti必出现在Tj之前。
这样,如果调度S的优先图中有环,则调度S是非冲突可串行化的,如果优先图中无环,则调度S是冲突可串行化的。
串行化顺序(serializability order)可通过拓扑排序(topological sorting,用于计算与优先图的偏序相一致的线形顺序)得到。一般而言,通过拓扑排序可以获得多个线形顺序。
因此,要判断冲突可串行化,需要构造优先图并调用一个环检测算法。基于深度优先的环检测算法需要n^2数量级的运算,其中n是优先图中的定点数(即事务数)。
有可能存在两个调度,它们产生的结果相同,但它们不是冲突等价的。比如下面的例子:
利用前面提到的优先图判定方法,上图的调度S并不与串行调度
不管是什么原因,如果事务Ti失败了,我们必须撤销该事务的影响以确保其原子性。在允许并发执行的系统中,原子性要求依赖于Ti的任何事务Tj(即Tj读取了Ti写的数据)也中止。为了确保这一点我们需要对系统所允许的调度类型做一些限制。
如下所示的调度,事物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提交之后再提交。
即使一个调度是可恢复的,要从事务Ti的故障中正确恢复,可能需要回滚若干事务。当其它事务读取了Ti写入的数据项时就会发生这种情况。下面调度中,如果T1发生故障,回滚。由于T2读取了T1写入的数据A,T2必须回滚。同理,T3也必须回滚。这种因单个事务故障导致一系列事务回滚的现象称为级联回滚(cascading rollback)。
级联回滚导致大量的撤销工作,这是我们不希望的。所以要对调度进行限制,避免级联回滚发生,这样的调度称为无级联调度。规范地说,无级联调度(cascadeless schedule)必须满足:对于事务Ti和Tj,如果Tj读取了先前由Ti所写的数据项,则Ti必须在Tj这一读操作之前提交。
容易理解,一个无级联调度也是可恢复调度。
从上到下,隔离级别依次提高。每个隔离级别的定义和解释中,说的都是该级别的最低要求。
所有的隔离级别都不允许脏写(dirty write),即如果一个数据项已经被另外一个未提交或者终止的事务写入,则不允许其它事务对该数据项进行写操作。
实现上,大多数数据库默认的事务隔离级别是Read committed,比如Sql Server , Oracle。Mysql的默认隔离级别是Repeatable read。
SQL中,可以显示设定事务的隔离级别。如可以通过语句set transaction isolation level serializable;
来显示将隔离级别设置为可串行化。另外,修改事务隔离级别必须作为事务的第一条语句执行。