事务
一,什么是事务?
构成单一逻辑工作单元的操作集合。是访问并且可能更新各种数据项的一个程序执行单元。事务用形如begin transaction 和 end transaction 界定,并且由其之间的全部操作组成。
二,事务的四大特性(ACID)
用一个简单事务模型来说明事务的各个特性。
设事务T为用户A给B转50$。
T |
read(A) |
A=A-50 |
write(A) |
read(B) |
B=B+50 |
write(B) |
整个过程为A账户先减少50$,然后B账户增加 50$。整个过程为一个事务。
①原子性(atomicity)
整个过程看成一个事务。如果 在A账户钱减少之后,也就是write(A)完成后,系统出现故障。那么整个系统将会丢失50$,这显然是不行的。所以我们说事务的原子性就是一个事务要么不开始,要么开始就保证一定完成。如何在即使系统故障的情况下保证事务的原子性呢?可以在事务开始前将整个流程记录在一个叫做日子文件的磁盘上,如果系统发生故障,我们可以通过日子文件将事务还原到哦最初的状态。
②一致性(consistency)
前面说到,当系统出现 故障的时候,整个系统将会丢失50$,即事务开始前A+B 不等于 事务执行后(发生故障)的A+B,我们说事务是不一致的,显然不一致是我们不想看到的。然而,一个事务在执行过程中必然发生不一致性(如在A账户减少50$的时候此时B账户还未增加50$)。在这里一致性为事务T执行前后系统A B 的和不变。
③隔离性(isolation)
当多个是事务并发执行时候,事务之间可能会交叉执行,可能会导致不一致的状态。如事务T在执行write(A)后,另外一个并发是事务也在读取A B和的值,并求和,显然的结果是不一致的,因为此时B账户还未增加余额。进一步如果该事务将A+B的值存入系统中,即使T事务后面完成,最后系统的数据肯定的不一致的。隔离性保证多个事务并发执行是不会产生不一致的结果。一种非常长简单的方法是,让事务串行的执行即事务一个接一个的执行,然而这要做效率是相当低的因为一个事务执行前必须等待前一个事务执行完后。
④持久性(durability)
持久性就是一旦事务已经完成,事务的操作必定已经到数据库(磁盘)里面去了,即使之后系统发生故障,数据库信息也不会丢失了。显然磁盘上的信息不想内存中,即使断电也不受到任何影响。
三,事务各类状态
活动状态:事务正在执行操作的状态。
部分提交状态:事务已经执行完最后一条语句。
失败状态:发现正常的执行不能再继续下去了。
中止状态:事务已经回滚并且数据库恢复为原来事务开始之前的状态。
提交状态:事务成功完成后提交(committed)。
事务最初开始执行,执行到某条语句发现执行不下去了或者所有语句都执行完了系统 断电了,(系统开始恢复)于是进入中止。否则事务正常执行完成并且提交。
1,关于事务中止以及回滚
事务中止回滚是通过存储再磁盘上的日志文件进行维护的,一旦出于某些原因导致事务中止,则系统通过日志上相关信息,使得系统回到事务开始之前的状态。日志(log)中存储了相关事务的标识符以及被更新数据项的标识符,以及该项的旧值和新值(修改后)。只有数据被写入日志后,事务才开始执行,这也保证了事务原子性。
2,补偿事务
一旦事务成功的提交后,事务是不可能通过中止回滚到最初事务开始时候的样子的,只有通过另外开启一个事务,来“抵消“以前的事务,另外开启的事务为补偿事务。如:将A账户金额减去50$的事务T成功提交,想回到T开始前,只能通过另外开启一个事务T'将A账户金额减去50$。
3,可见外部写
有些事务可能需要将数据输出到屏幕,或者其他外部可见的设备上,并且在事务执行过程中,一旦事务输出到外部后,是不可一撤消的,即便是事务并没有提交,但系统出现故障。解决这类问题办法是先将要输出的数据写入非易失性存储器上,如磁盘。然后在事务成功提交后再将数据输出到外部,即便是数据输出一半后系统出现故障导致数据不能再输出,我们可以在系统恢复后,再次确认先前事务是否提交,然后从磁盘上将数据向外部输出。避免了数据输出一半而事务停止而无法回滚的情况。
四,事务的隔离性
先前提到,事务的隔离性可以通过,事务串行的执行即一个接一个事务的执行来实现。但其会降低系统效率,比如,有些事务涉及一些非常耗时的I/O操作,使得后面的事务不得不等到其完成后方可开始,这使得CPU资源的极大浪费。所以我们需要研究如何使得事务并发执行,但又不会数据库的结果的方法即使并发控制机制,来提高系统资源利用率以及减少事务等待时间。
调度
事务是由一系列指令组成的,我们称这些指令的按照时间执行的顺序称为调度,一组事务的调度必须包含该组事务的全部指令
可以看到事务T1,将A账户减去50¥......,事务T2将A账户增加10%......,按照T1,T2的顺序执行的,我们认为其是串行的。
T1 |
T2 |
read(A) A=A-50 write(A) read(B) B=B+50 write(B) commit |
read(A) A=A+A*0.1 write(A) read(B) B=B-100 write(B) commit |
现在我们将其进行并发调度的到如下调度
T1 |
T2 |
read(A) A=A-50 write(A)
read(B) B=B+50 write(B) commit |
read(A) A=A+A*0.1 write(A)
read(B) B=B-100 write(B) commit |
可以通过计算可以得出第一个串行执行的调度与第二个并发调度得到的系统最终的A+B值是相同的。
事务的串行调度
事务按照先后顺序进行执行
事务的并发调度
再宏观上来事务是一个接一个的执行即串行进行的,但微观其是交叉执行的,如以上的第二个调度
事务的可串行化
某一种并发调度再某种意义上等价于一个串行调度。如以上例子中的后者等价于前者。可串行化的并发调度一定为正确的调度,但正确的调度不一定为可串行化调度。
见如下例子。
T1 |
T2 |
read(A) A=A-50 write(A)
read(B) B=B+50 write(B) commit |
read(B) B=B-100 write(B)
read(A) A=A+100 write(A) commit |
0x111
这个并发调度,与串行调度<T1,T2>最终的到的结果是相同的。但是该调度不是可串行化的。后面将进一步说明。
我们先说下调度中指令中的冲突的概念。假设在并行调度S中,某连续指令I,J(进行read或wirte操作的指令),当这两条指令引用的是不同的数据项的时候,交换该指令的顺序是不会影响到系统最终的结果。但是若是引用相同数据项Q,则有很多情况。
1,当两条指令都是read(Q) ,即使交换顺序也无关紧要,因为都是读的相同的数据。
2,如果一条是write(Q),一条是read(Q),则其先后顺序是重要的,在前面的事务应该先读或则写,在后面的事务应该后读或写。
3,两条都是write(Q),先后顺序也很重要不能随便改动,因为在后面的write(Q)指令必然会将前面的write(Q)值覆盖,系统最后在 那个得到的也必然是最后的write(Q),因此与前面的一样在前面的事务应该先执行write后面的则后执行。
冲突
当两条指令为不同事务操作相同的数据项的时候,任何至少有一条指令为write()的时候,我们认为这两条指令是冲突的,因为他们的顺序会影响到系统最终的结果。当两条指令都是read()的时候,则不会产生冲突。
冲突可串行化
看如下 调度1
T1 |
T2 |
read(A) write(A)
read(B)
write(B) |
read(A)
write(A)
read(B) write(B) |
我们可以进行一些非冲突指令交换得到新的调度。(T1:read(B) 与 T2:read(A) 交换,都为读且数据项不同)→(T1:write(B) 与 T2: write(A) 交换)→(T1:write(B) 与 read(A) 交换)。
T1 |
T2 |
read(A) write(A) read(B) write(B) |
read(A) write(A) read(B) write(B) |
如果调度S通过一系列非冲突指令交换得到调度S`,我们认为其为冲突等价的。当某调度冲突等价于串行调度我们认为该调度为冲突可串行化调度。故非冲突可串行化则为,调度S可以经过一系列冲突指令交换得到一个串行化调度。前面说到正确的调度不一定是可串行化调度,前面的调度(0x111)就是一个典型的非冲突可传性化的调度,但他是正确的调度,应为该调度与串行化调度最终得到的系统值是相同的。
这里我们基于判断调度是否为冲突可串行化的,给出了一个简单的方法,(事务的)优先图。
图中事务T1箭头指向T2,表明事务T1,先于事务T2执行。并且只有在如下情况下一个事务会箭头指向另一个事务(假设操作都是同一个数据项Q)。
1,T2执行read或者write之前,T1执行write
2,T2执行write之前,T1执行read
(可自行理会,相当明了。)
当出现如下调度时,可产生优先图。
T1 |
T2 |
read(Q) write(Q) |
write(Q)
read(Q) |
T1:在read(Q)之前,T2:write(Q) ,有T2→T1。T2:在read(Q)之前,T1write(Q) ,有T1→T2。
此时形成了所谓的环,我们并不知道谁先谁后。所以我们认为该优先有环图是非冲突可串行化的。而无环的优先图则为冲突可串行化的。
有了优先图,于是我们就可以通过检测图中是否存在环路来判定调度的冲突可串行化。而且我们还能基于优先图,进行拓扑排序算法得到事务的先后顺序。
如图优先图我们可以得到两种事务顺序。{T1 T2 T3 T4}或{T1 T3 T2 T4}
五,事务的隔离级别
【注意:个人认为事务的隔离级别比较重要的并且在很多地方用的着的。】
1,数据库中几种常见的不一致性
①丢失修改
T1 |
T2 |
read(Q)
write(Q-1) commited |
read(Q) write(Q-1) commited
|
考虑如下例子,火车售票总票数为Q=10,A B两人同时网上订票,A读取票时Q=10,B读取票时Q=10,此时B预定票成功并将Q-1 = 9后写入数据库提交事务,而A在B提交事务后,也预定票成功Q = 10 -1 = 9,也写入数据库。此时数据库总票数为9张但是A,B两个人都订了票,从而导致数据库数据不一致性。A数据对数据库的修改被丢弃,从而导致了们所说的修改丢失。
②不可重复读
事务T1从数据库中读取某数据项之后,事务T2对数据库中的该项数据进行了更新或则修改导致,事务T1再次从数据库中读取该数据项时与先前读取的数据结果不一致的情况叫做不可重复读。
③脏读
事务T1更改数据库中的数据项Q并写如磁盘但是事务并未提交,此时事务T2从数据库中读取该数据项Q,之后又因为某些原因事务T1被撤销,从而导致事务T2读取错误的数据,叫做脏读。
④幻读
事务T1从数据库中读取关系中某些记录后,另外的事务T2对关系中插入或者删除某些记录,当事务T1再次读取关系的时候,关系中莫名奇妙的增加了记录或则消失了记录的现象叫做幻读。
2,事务的隔离级别
【等级从低到高】
①未提交读
允许读取还未提交的数据项,不允许丢失修改。
②已提交读
只允许读取已提交的数据项,不允许修改丢失,允许重复读错误。
③可重复读
只允许读取已提交数据项,不允许修改丢失,不允许不可重复读错误。
④可串行化
保证调度是可串行调度。
各隔离等级下的要求
|
丢失修改 |
脏读 |
重复读错误 |
幻读 |
未提交读 |
不允许 |
允许 |
允许 |
允许 |
已提交读 |
不允许 |
不允许 |
允许 |
允许 |
可重复读 |
不允许 |
不允许 |
不允许 |
允许 |
可串行化 |
不允许 |
不允许 |
不允许 |
不允许 |
六,隔离级别的实现**
1,基于锁的隔离级别实现
共享锁(S)
某数据项被事务加上共享锁(S)后,该数据项可以被该事务读但是不允许写。
排他锁(X)
某数据项被加上排他锁(X)后,该事务可以对该数据项进行读写操作。
相容矩阵
当某事务对某一数据项加上锁后,其他事务再次申请该数据项上的锁时,是否给予该事务申请的在该数据项上的锁
见如下共享锁和排他锁相容矩阵 。当事务T1拥有Q上的共享锁时,其他事务申请该数据项上的共享锁时是能够被授予的,但是若申请该数据项上的排他锁时则不被授予。
|
共享锁 |
排他锁 |
共享锁 |
授予 |
不授予 |
排他锁 |
不授予 |
不授予 |
封锁协议
协议其实就是一组规则规定。至于封锁协议则是规定事务何时对数据项进行加锁,解锁的一组规则。
事务饿死现象
考虑一种情况,事务T1在数据项上Q上拥有共享锁,接着事务T2申请Q上的排他锁,根据相容矩阵可知道系统时拒绝的,因此事务T2不得不等到事务T1释放在该数据项上的锁在进行加锁,T2等待。此时有来了一个事务T3,他申请数据项Q上的共享锁,根据相容矩阵他被授予该锁,此时T1释放了Q上的共享锁,T2再次尝试获取该数据项上的排他锁,但是此时T3拥有该数据项上的共享锁,所以T1有被拒绝,并且不得不等待T3,如果后面,又接着来了事务T4,T5....申请该数据项上的共享锁,那么T1岂不是永远无法获得该数据项上的排他锁了,此时T1处于饿死状态。我们当然不希望有这种情况,我们可以通过事务先来后到的顺序来给予同一数据项上的锁,来解决此类问题。
我们来简单看下锁管理器时如何实现,并避免饿死现象的发生 。
有图我们可以看出锁管理器通过锁表,索引到各个数据项,其实通过散列来实现的,然后每个数据项又维护一个链表,该链表节点对应各个事务以及事务获得的锁,后来的事务则通过加在链表末尾,当事务释放其拥有的锁的时候事务从链表中删除。我们看最下面的数据项E1,他维护一个T1 T5 T9 T2 的链表,红色的字体表示是排他锁,黑色的字体为排他锁。T1,T5申请的是共享锁因此他们都获得了该数据项上的锁,并且没有释放,T9申请的是排他锁,因此他等待他前面事务的锁的释放,T2虽然申请的共享锁但是他前面有事务T9正在申请排他锁,因此他要等到T9获得锁并释放锁才能获得锁。可见这里T9不会出项饿死状态,因此避免了前面提到的情况。
现在我们通过封锁协议的加锁与解锁时机来实现四类隔离级别
1,未提交读
当事务T1对数据项Q进行修改时,对其加上排他锁防止其他事务对Q进行修改,并且修改完成后释放该锁。仅防止了修改丢失错误。 其也会可能会产生事务的级联回滚。我们简单来说一下级联回滚,依据以上的读在T1unlock(Q)那一刻,事务T2,获得共享锁并且read(Q),在这之后事务T1中止了,因此事务T1需要回滚,但T2读取的数据时基于T1写的Q(假设T1在对Q加锁期间对Q进行写操作),因此事务T2也要回滚这就时我们所说的级联回滚,这个种情况也可以发生在多个事务之间。想象一下一种更加糟糕的情况,假若事务T2在T1中止之前就已经提交事务了,此时即使事务T1中止,事务T2也无法回滚了,我们认为这样的调度时不可恢复的调度,可恢复调度则与之相反。虽然这种情况是我们不希望看到的也可以预防,假若遇到这样的情况我们也可以通过前面所说的补偿事务来解决。
2,已提交读
当事务T1对数据项Q进行修改时,对其加上排他锁防止其他事务对Q进行修改,并且在该事务提交后释放该锁。可以防止修改丢失和脏读,也不会发生级联现象和不可恢复调度。但是事务T1在中间时刻已经完成了对write(Q),write(Q)之后即使事务T1没有对Q进行任何操作任然占着Q的资源,从而导致了资源上的浪费。
3,不可重复读
有写要求的数据项Q加上排他锁直到事务提交时再释放该锁,有读要求的数据项W加上共享锁直到不再读取时该事务释放该锁。防止修改丢失,脏读和可重复读错误。
4,可串行化
有写要求的数据项Q加上排他锁直到事务提交时再释放该锁,有读要求的数据也加上排他锁直到不再读取该事务时释放该锁。防止修改丢失,脏读,可重复读错误和幻读。
【注意:1,2并没有使用共享锁,3,4才开始使用共享锁】
很显然,隔离等级越高资源浪费的越多,系统的效率也相对较低。
两阶段封锁协议
将事务对数据项的加锁和解锁放在两个阶段。分别时增长阶段和收缩阶段,增长阶段只允许加锁,收缩阶段只允许解锁。在这两个阶段分界点为封锁点,也就是第一个解锁的时刻,之后就不能再进行加锁了。两阶段封锁协议保证了调度是可串行化的,但是也同样也可能存在死锁和级联的风险。
证明两阶段封锁协议是可串行化的
证明过程可以用到前面的说过的优先图,我们通过判断是否成环来判断是否是冲突可串行化的。假设事务集合S 满足两阶段封锁协议的,不妨再假设S中存在成环事务优先图,即T1→T2→T3...→Tn→T1,则必定存在L1(A) U1(A) L2(A) L2(B) U2(A) U2(B) L3(B) L3(C) U3(B) U3(C) ... Ln(X) Ln(Y) Un(X) Un(Y) L1(Y) U1(Y) 。这里我们假定事务T1和事务T2,有冲突操作即操作了相同的数据(其中一个为写操作) A 则T2必须等到T1释放对A锁才能操作A,同样的T3等待T2释放对B的锁...T1等到Tn释放对Y的锁,由于事务是遵顼两阶段封锁协议的即一个事务第一次释放锁后,不能再有加锁动作,显然这里事务T1与之矛盾,所以事务集S中是不存在成环的优先图的,也就是冲突可串行化的。
两阶段封锁协议产生死锁的情况
T1 |
T2 |
Lock(A) Read(A) Write(A)
Lock(B) …wait |
Lock(B) Read(B) Write(B) Lock(A) …..wait |
很显然事务T1和事务T2都遵循两阶段封锁协议,但是T2等待T1释放A锁,T1等待T2释放B锁,从而产生了死锁的情况。
两阶段封锁协议级联的情况
T1 |
T2 |
T3 |
Lock(A) Read(A) Write(A) Unlock(A)
|
Lock(A) Read(A) Write(A) Unlock(A) |
Lock(A) Read(A) |
可以看到事务T1在还没有提交的时候已经释放了对于A的锁然后是事务T2,T3,当事务T3执行Read操作之后然而事务T1因为某些原因中止了而导致事务回滚此时的回滚是级联的即T2,T3也必须回滚,因为T2,T3都读取了T1写的数据。
两阶段封锁协议防止级联的一种方法是严格两阶段封锁协议,即事务持有的排他锁都在事务提交之后释放。这样保证了事务不能在别的事务持有该事务要访问的数据的排他锁释放之前(即事务提交之前)获得该锁,这样就防止了级联的发生。还有一种叫做强两阶段封锁协议,即事务的任何锁(排他锁或者共享锁)必须在事务提交后释放,强两阶段封锁协议是按照提交的顺序串行化的。
锁转化
可以想象一种场景,事务T1开始的时候对某写数据项有读的要求,由于之后还要对这些数据项进行写。所以一开始就获得这些数据项上的排他锁,先对这些数据项进行读操作,中间一段时间内不对这些数据项进行任何操作,之和再对这些数据项进行写操作。由于该事务时遵顼两阶段封锁协议的,事务T1只能在写操作结束之后才能释放这些数据项上的锁,由于中间这段时间内持有排他锁因此其他在他前面的事务不等并发的得这些数据项上的共享锁,且必须等到其释放锁后方可读,显然这降低了并发度。于是我们想是不是可以一开始事务T1只对这些数据项上的共享锁,之后有写要求的时候在将其升级为排他锁,于是就有了锁转化。
锁转换有升级(共享锁变排他锁)和降级(排他锁变共享锁),且升级只能发生在增长阶段,降级只能发生收缩阶段。
两阶段封锁协议小结
1,不存在修改丢失问题
2,脏读情况。比如:T1(lock-X(A) write(A) unlock-X(A))→T2(lock-S(A) read(A) unlock-S(A)) →T1(...由于某些原因中止),T2也必须回滚否则脏读(即级联)。如果是严格两阶段封锁协议则不会发生以上情况(即不会产生级联现象,前面提到过)。强两阶段封锁协议更加不会。
3,可重复读错误情况,不会发生。
4,商业上一般使用的是严格两阶段封锁协议加锁转换。保证事务无级联且是可串行化的,也有较高的并发度(锁转换)。
多粒度
百度一下:粒度是指数据仓库的数据单位中保存数据的细化或综合程度的级别。细化程度越高,粒度级就越小;相反,细化程度越低,粒度级就越大。数据的粒度一直是一个设计问题。
我们简单来了解一下粒度,之前我们讨论的事务对数据的操作都是在数据项级别上的,也就是说粒度是也可以说是最小的,细化程度高嘛。我们之前也讨论过幻读现象,比如说事务T1在查看某张表中满足条件的数据项并且对其加上了锁,事务T2在关系中插入一个满足T1查询条件的数据,导致表中多出了一条数据,如产生幻觉一般。但是如果我们将锁的粒度加大到表这个范围(即将整个表锁住),则不会产生这样的情况,因为T1已经持有表的锁了,所以T2就不能在对该表进操作了。其实不仅有表锁还有文件上的锁,以及数据库上的锁等。
当我们想操作整个关系的时候,并且不想别的事务访问,我们可以给这个关系加上排他锁。但是有没有想到一种情况 ,假若我想给整个数据库DB加上锁,比如排他锁,但此时其他的事务已经对关系r1加上了排他锁,所以我们不能成功的申请到对整个数据库的排他锁,那么我们是怎么知道r1上还有排他锁的呢,我们必须便利整个数据库,甚至是关系中的的每一个数据项是否加上了锁,显然这样遍历整个数据库是我们不太像要的,因为这样的效率极低。因此我们想到了一个新的方法,即意向锁。何为意向锁?意向锁怎样工作?待我后面娓娓道来。
意向锁的含义是如果对一个结点加意向锁,则说明该结点的下层结点正在被加锁;对任一结点加锁时,必须先对它的上层所有祖先结点加意向锁。
有了意向锁,当我们判断是否可以对一个节点加上锁时,我们可以先判断想加锁的节点的意向锁是否与我们想给节点加的锁互斥,然后看是否能获得从根节点到目标节点路径上的每一个 节点的意向锁。总的来说我们一开始判断目标节点是为了检查目标节点的所有子节点加上了什么类型的锁,而从根节点到目标节点路劲检查时为了检查目标节点的所有父节点加上了什么锁,最后来判断是否可以给其加上锁。显然这样极大的节约了时间,因为我们不需要扫描整个树。
意向锁类型
排他意向锁(IX)
当一个节点加上了IX,我们认为该节点的所有子节点中必然存在某个节点或者某些节点加上了排他锁。
共享意向锁(IS)
同样的当一个节点加上了共享意向锁时,我们认为他的所有子节点中必然存在某个节点或某些节点加上了共享锁。
共享排他意向锁(SIX)
当一个事务想给一个节点加上共享锁同时又想修改该节点下某个子节点,我们可以给该节点加上 SIX。
相容矩阵
|
共享锁 |
排他锁 |
共享意向锁 |
排他意向锁 |
共享排他意向锁 |
共享锁 |
授予 |
不授予 |
授予 |
不授予 |
不授予 |
排他锁 |
不授予 |
不授予 |
不授予 |
不授予 |
不授予 |
共享意向锁 |
授予 |
不授予 |
授予 |
授予 |
授予 |
排他意向锁 |
不授予 |
不授予 |
授予 |
授予 |
不授予 |
共享排他意向锁 |
不授予 |
不授予 |
授予 |
不授予 |
不授予 |
作为一个简单的示例,我们来简单了解意向锁的工作过程,用到上面的树状图。
①某个事务T1申请关系r1上的排他锁,他先检查该节点上的意向锁,显然没有任何锁,然后看是否能获得从根节点到该节点路径上的所有节点的IX锁,(如果不冲突的化)加上IX锁,因为此时没有任何锁所以该事务T1申请的锁授予。
②当一个事务申请File2上的共享锁时,有如下过程,先检查File2上是否有锁与之冲突,显然没有,然后获得从根节点到该节点路径上的所有节点的IS锁,并判断是否有与之冲突,比如这里DB上有IX锁,显然与我想获得IS锁是无冲突的,最后授予FIle2S锁。得到如下图。
③此时某个事务申请File1上的共享锁,先检查File1上的锁是否与之冲突,显然IX与S是冲突的,所以该事务申请直接就失败了。
死锁
前面的例子中我们也提到过死锁的情况,死锁是在并发系统中非常常见的问题。有如下定义,在事务集合S中,每个事务Ti都在等待其他事务的锁的情况,我们认为出现了死锁。
对于死锁我们要们预防他的发生要么发生后对其进行恢复。预防:比如说我们一旦发现某个事务在等待加锁并可能发生死锁,我 们立即回滚该事务。再比如说对某个事务我们一开始就获得该事务的所有需要的锁,然后进行操作。虽说这些方法是有效的但也是低效的。恢复:当某些事务发生死锁时我们回滚其中的一个或某些事务使其恢复。还有一种介于预防和恢复之间的机制叫锁超时,当某个事务等待某个锁超过了一定的时间,我们就回滚该事务。实际上死锁的处理有很多算法以及机制,这里将的比较笼统。
2,基于时间戳的的协议
时间戳
不严谨的来说其实就是某个时刻,当事务T1启动的时候,系统将该时刻给事务T1,即为事务T1的时间戳TS(T1)。当有新的事务T2进来的时候,事务T2也被给予时间戳TS(T2),由于T1的时间戳比T2的时间戳小,因此事务T1必然是在事务T2,前面执行的事务。时间戳有唯一递增的特点。利用时间戳可以进行并发控制。
如何利用时间戳进行并发控制,即多个事务并发执行的时候保证其冲突可串行性呢?这里有一种简单的规则。
R-timestamp(X) : 表示成功读取数据项X的所有事务中最大事务时间戳。
W-timestamp(X) : 表示成写入数据项X的所有事务中的最大事务时间戳。
①事务T1执行读操作 read(X) 时
如果TS(T1) < W-timestamp(X) ,即存在事务T1后面的事务已经成功写入数据项X,因此T1读取数据过晚,系统拒绝T1的 read(X)请求,因为T1 想读取的数据应该是在他之前的事务写入的数据项X的值,因此事务T1回滚,重启。
如果TS(T1) >= W-timestamp(X),事务T1成功read(X),这里等于说明是该事务本身,并且将R-timestamp(X)设置为MAX(R- timestamp(X),TS(T1)),即更新R-timestamp(X)的值。
①事务T1执行读操作 write(X) 时
如果TS(T1) < W-timestamp(X),即存在事务T1后面的事务已经成功写入数据项X,因此T1写入数据过晚,系统拒绝T1的 write(X) 请求,因为事务T1读取若写入X将会覆盖掉他后面的事务已经写入的数据项,因此事务T1回滚重启。
如果TS(T1) < R-timestamp(X) ,即存在事务T1后面的事务已经读取了数据项X的值且读的不是他写入的值,并且认为事务T1已 经不存在,因此T1写入数据过晚,事务T1必须回滚,重启。
如果文字描述的不够清楚可以结合下面来理解。
基于时间戳协议的并行事务是不会发生死锁的情况的。任何事务在执行指令的过程中总是会先判断是否和先前的事务发生冲突,即根据上面的简单的规则,一旦发生冲突立马回滚,没有等待,没有等待就没有死锁。
基于时间戳的协议级联并行事务会不能保证无级联现象。某个事务发出read(Q)请求,发现最后写Q的事务的时间戳在他之前,因此他可以read(Q)操作,但是由于某些原因之前的