第十二章 事务管理
事务是访问并可能更新各种数据项的一个程序执行单元。
这些操作要么都做,要么都不做,是一个不可分割的工作单位。例如银行转账
Commitwork表示提交,事务正常结束。
Rollbackwork表示事务非正常结束,撤消事务已完成的操作,回滚到事务开始时状态。
read(X):从数据库传送数据项X到事务的工作区中
write(X):从事务的工作区中将数据项X写回数据库
原子性(Atomicity)
事务中包含的所有操作要么全做,要么全不做。原子性由恢复系统实现。
一致性(Consistency)
事务的隔离执行必须保证数据库的一致性。事务开始前,数据库处于一致性的状态;事务结束后,数据库必须仍处于一致性状态;事务的执行过程中可以暂时的不一致。数据库的一致性状态由用户来负责,由并发控制系统实现。
如银行转账,转账前后两个帐户金额之和应保持不变。
隔离性(Isolation)
系统必须保证事务不受其它并发执行事务的影响。对任何一对事务T1,T2,在T1看来,T2要么在T1开始之前已经结束,要么在T1完成之后再开始执行。隔离性通过并发控制系统实现。
持久性(Durability)
一个事务一旦提交之后,它对数据库的影响必须是永久的。系统发生故障不能改变事务的持久性。持久性通过恢复系统实现。
基本比较:
并行事务会破坏数据库的一致性,串行事务效率低。
并行的优点:
一个事务由不同的步骤组成,所涉及的系统资源也不同。这些步骤可以并发执行,以提高系统的吞吐量(throughput)。
系统中存在着周期不等的各种事务,串行会导致难于预测的延迟。如果各个事务所涉及的是数据库的不同部分,采用并行会减少平均响应时间(average response time)。
核心问题:
在保证一致性的前提下最大限度地提高并发度。
丢失修改(lost update)
不可重复读(non-repeatable read)
读“脏”数据(dirty read)
事务1与事务2从数据库中读入同一数据并修改,事务2的提交结果破坏了事务1提交的结果,导致事务1的修改被丢失。
事务1读取数据后,事务2执行更新操作,使事务1无法再现前一次读取结果。
三类不可重复读:
事务1读取某一数据后:
1.事务2对其做了修改,当事务1再次读该数据时,得到与前一次不同的值。
2.事务2删除了其中部分记录,当事务1再次读取数据时,发现某些记录神秘地消失了。
3.事务2插入了一些记录,当事务1再次按相同条件读取数据时,发现多了一些记录。
后两种不可重复读有时也称为幻影现象(phantomrow)。
事务1修改某一数据,并将其写回磁盘,事务2读取同一数据后,事务1由于某种原因被撤消,这时事务1已修改过的数据恢复原值,事务2读到的数据就与数据库中的数据不一致,是不正确的数据,又称为“脏”数据。
事务的执行顺序称为一个调度(schedule),表示事务的指令在系统中执行的时间顺序。
当并行调度等价于某个串行调度时,则称它是正确的。
冲突指令:
当两条指令是不同事务在相同数据项上的操作,并且其中至少有一个是write指令时,则称这两条指令是冲突的。
冲突等价:
如果调度S可以经过一系列非冲突指令交换转换成调度S',则称调度S与S'是冲突等价的(conflictequivalent)。
冲突可串行化:
当一个调度S与一个串行调度冲突等价时,则称该调度S是冲突可串行化的(conflict serializable)。
冲突可串行化判定:优先图。
事务的恢复:
一个事务失败了,应该能够撤消该事务对数据库的影响。如果有其它事务读取了失败事务写入的数据,则该事务也应该撤消。
可恢复调度:
对于每对事务T1与T2,如果T2读取了T1所写的数据,则T1必须先于T2提交。
级联调度:
由于一个事务故障而导致一系列事务回滚。
无级联调度:
对于每对事务T1与T2,如果T2读取了T1所写的数据,则T1必须在T2读取之前提交。
<注>无级联调度必是可恢复调度
按照隔离级别从低到高的顺序:
未提交读:允许读取未提交数据。(当事务A更新某条数据时,不容许其他事务来更新该数据,但可以读取。)
已提交读:只允许读取已提交数据,但不要求可重复读。(当事务A更新某条数据时,不容许其他事务进行任何操作包括读取,但事务A读取时,其他事务可以进行读取、更新。)
可重复读:只允许读取已提交数据,而且一个事务两次读取一个数据项期间,其他事务不得更新该数据,但是该事务不要求与其他事务可串行化。
可串行化:保证可串行化调度。
隔离级别 |
肮脏读取 |
不可重复读取 |
幻象读取 |
未授权读取 |
可能发生 |
可能发生 |
可能发生 |
授权读取 |
- |
可能发生 |
可能发生 |
可重复读取 |
- |
- |
可能发生 |
可序列化 |
- |
- |
- |
并发控制机制的任务:对并发操作进行正确调度、保证事务的隔离性、保证数据库的一致性。
基本封锁类型:
排它锁(exclusive lock,简记为X锁)
共享锁(Share lock,简记为S锁)
共享锁又称为读锁。若事务T对数据对象Q加上S锁,事务T可读但不能写Q,其它事务只能再对Q加S锁,而不能加X锁,直到T释放Q上的S锁。
排它锁又称为写锁。若事务T对数据对象Q加上X锁,则事务T既可以读又可以写Q,其它任何事务都不能再对Q加任何类型的锁,直到T释放A上的锁。
饥饿/饿死:
不断出现的申请并获得S锁的事务,使申请X锁的事务一直处在等待状态。
饥饿的防止:
对申请S锁的事务,如果有先于该事务且等待的加X锁的事务,令申请S锁的事务等待。
定义:每个事务分两个阶段提出加锁和解锁申请。
增长阶段(growing phase):事务可以获得锁,但不能释放锁。
缩减阶段(shrinking phase):事务可以释放锁,但不能获得新锁。
封锁点(lock point):事务最后加锁的位置,称为事务的封锁点, 记作Lp(T)。
并行执行的所有事务均遵守两段锁协议,则对这些事务的所有并行调度策略都是可串行化的。所有遵守两段锁协议的事务,其并行执行的结果一定是正确的。
事务遵守两段锁协议是可串行化调度的充分条件,而不是必要条件。可串行化的调度中,不一定所有事务都必须符合两段锁协议。
两阶段封锁协议不保证不会发生死锁。
严格两阶段封锁协议:除了要求封锁是两阶段之外,还要求事务持有的所有排他锁必须在事务结束后,方可释放。
强两阶段封锁协议:事务提交之前,不得释放任何锁。在强两阶段封锁协议下,事务可以按其结束的顺序串行化。
Upgrade:从共享锁提升为排他锁
Downgrade:从排他锁降级为共享锁
锁升级只能发生在增长阶段,锁降级只能发生在缩减阶段。
事务访问数据的粒度不同
DB、Table、Tuple、…
单一封锁粒度的问题
封锁粒度大:并发性低
封锁粒度小:访问大粒度数据加锁量巨大
多粒度封锁
根据访问数据的粒度,确定封锁的粒度
以求加锁量有限,并可获得最大的并发性
大粒度数据由小粒度数据组成;
允许对不同粒度数据进行封锁;
事务对大粒度数据加锁,隐含地对组成大粒度数据的所有小粒度数据加锁。
多粒度层次树,子节点表示的数据是父节点表示数据的一部分。
如果一个节点加上了意向锁,则意味着要在树的较低层进行显示加锁。
在一个节点显式加锁之前,该结点的全部祖先均加上了意向锁。
事务判定是否能够成功地给一个结点加锁时,不必搜索整棵树。
共享意向锁(IS)/排他意向锁(IX)/共享排他意向锁(SIX)
遵从锁的相容矩阵;
根结点必须首先加锁,可以加任何类型的锁;
仅当Ti对Q的父结点持有IX或IS锁时,Ti对于结点Q加S或者Is锁;
仅当Ti对Q的父结点持有IX或SIX锁时, Ti对于结点Q加X、SIX、IX锁;
仅当Ti未曾对任何结点解锁时,Ti可以对结点加锁(两阶段的);
仅当Ti当前不持有Q的子节点的锁时,Ti可以对节点Q解锁。
特点:
增加了并发行,减少了锁开销。
适应范围:
只存取几个数据项的短事务,
由整个文件或一组文件形成报表的长事务。
时间戳排序协议的目标:
令调度冲突等价于按照事务开始早晚次序排序的串行调度。
时间戳排序协议的基本思想:
开始早的事务不能读开始晚的事务写的数据;
开始早的事务不能写开始晚的事务已经读过或写过的数据。
W-timestamp(Q):表示成功执行write(Q)的所有事务的最大的时间戳。
R-timestamp(Q):表示成功执行read(Q)的所有事务的最大的时间戳。
注意:不是最后执行Read(Q)的事务的时间戳
例如:TS(T1)=1;TS(T2)=2;
T2:read(Q) //r-ts(Q)=2
T1:read(Q) //r-ts(Q)=2 (≠1!)
保证冲突可串行化;
冲突可串行化的调度不一定能被时间戳排序协议调度出来;
无死锁;
存在饥饿/饿死现象(事务可能被反复回滚、重启);
不能保证可恢复性(可以扩展协议以保证可恢复性,如跟踪提交依赖等)。
每个事务Ti在其生存期中按两个或三个阶段执行:
1、读阶段:各数据项值被读入,并保存在事物Ti的局部变量中。
2、有效性检查阶段:判断是否可以将write操作所更新的临时局部变量值复制到数据库而不违反可串行性。
3、写阶段:若事务Ti已经通过有效性检查,进行实际的数据库更新,否则,回滚。
为了进行有效性检测测试,需要知道事务Ti的各个阶段何时进行,设立三个时间戳:
1、Start(Ti):事务Ti开始执行的时间。
2、Validation(Ti):事务Ti完成读阶段并开始其有效性检查阶段的时间。
3、Finish(Ti):事务Ti完成写阶段的时间。
基于日志的恢复