并发控制
可恢复调度
对于每对事务Ti和Tj,如果Tj读取了之前由Ti所写的数据项,则Ti先于Tj提交。
无级联调度
对于每对事务Ti和Tj,如果Tj读取了先前由Ti所写的数据项,则Ti必须在Tj这一读操作前提交。
锁
定义:令{T0,T1,.....,Tn}是参与调度S的一个事务集,如果存在数据项Q,使得Ti在Q上持有A型锁,后来,Tj在Q上持有B型锁,且comp(A,B)=false,则我们称在S中Ti先于(precede)Tj,记为Ti->Tj。如果Ti->Tj,这一居先意味着在任何等价的串行调度中,Ti必须出现在Tj之前。指令之间的冲突对应于锁类型之间的不相容性。
如果调度S是那些遵从封锁协议规则的事务集的可能调度之一,我们称调度S在给定的封锁协议下是合法的(legal)。当且仅当其所有合法的调度为冲突可串行化时,我们称一个封锁协议保证冲突可串行性;换句话说,对于任何合法的调度,其关联的关系是无环的。
两阶段锁
要求每个事务分两个阶段提出加锁和解锁申请。
- 增长阶段(growing phase):事务可以获得锁,但不能释放锁。
- 缩减阶段(shrinking phase):事务可以释放锁,但不能获得新锁。
缺点:依旧存在级联回滚
严格两阶段封锁协议(strict two-phase locking protocol)
要求事务所有的排他锁必须在事务提交后方可释放,避免级联回滚。
强两阶段封锁协议(rigorous two-phase locking protocol)
要求事务提交之前不得释放任何锁。
树形协议(tree protocol)
要求所有的数据项集合D={d1,d2,...,dn}满足偏序->:如果di->dj,则任何既访问di对访问dj的事务必须先访问di,然后访问dj。这种偏序可以是数据的逻辑或物理组织的结果,也可以只是为了并发控制而加上的。
![image-20211105222137474](/Users/quehualin/Library/Application Support/typora-user-images/image-20211105222137474.png)
- Ti第一个加锁可以对任何数据项进行。
- 此后,Ti对数据项Q加锁的前提是Ti当前持有Q的父项上的锁。
- 对数据项解锁可以随时进行。
- 数据项被Ti加锁并解锁后,Ti不能再对该数据项加锁。
不保证可恢复性和无级联回滚。为了保证可恢复性和无级联回滚,可以将协议修改为在事务结束前不允许释放排他锁。直到事务结束前一直持股排他锁降低了并发性。
这里有一个提高并发性的替代方案,但它只保证可恢复性:为每一个发生了未提交写操作的数据项,我们记录是哪个事务最后对它执行了操作,当事务Ti执行了对未提交数据项的读操作,我们就在最后对该数据项执行了写操作的事务上记录一个Ti的提交依赖(commit dependency),在有Ti依赖的所有事务提交完成之前,Ti不能提交。如果其中一个事务中止,Ti也必须中止。
多粒度锁
IS | IX | S | SIX | X | |
---|---|---|---|---|---|
IS | true | true | true | true | false |
IX | true | true | false | false | false |
S | true | false | true | false | false |
SIX | true | false | false | false | false |
X | false | false | false | flase | false |
对数据项Q加锁规则:
- 事务Ti必须遵从锁类型相容函数
- 事务Ti第一次封锁树的根结点,并且可以加任意类型的锁
- 仅当Ti当前对Q的父节点具有IX或IS锁时,Ti对结点Q可加S或者IS锁
- 仅当Ti当前对Q的父节点具有IX或SIX锁时,Ti对结点Q可加X、SIX或者IX锁
- 仅当Ti未曾对任何节点解锁时,Ti可对结点加锁(也就是说,Ti是两阶段的)
- 仅当Ti当前不持有Q的子节点的锁时,Ti可对节点Q解锁
多粒度协议要求加锁自顶向下的顺序(根到叶),而锁的释放则按自底向上的顺序(叶到根)
死锁预防
-
对加锁请求进行排序或要求同时获得所有的锁来保证不会发生循环等待
- 在事务开始前通常很难预知哪些数据项需要封锁
- 数据项使用率可能很低,因为许多数据项可能封锁很长时间却用不到
对所有的数据项强加一个次序,同时要求事务只能按次序规定的顺序封锁数据项。变种是使用数据项与两阶段关联的全序。一旦一个事务锁住了某个特定的数据项,它就不能申请顺序中位于该数据项前面的数据项上的锁。只要在事务开始执行之前,它要访问的数据项集是已知的,该机制就容易实现。如果使用了两阶段封锁,潜在的并发控制系统就不需要理性:所需要的是,保证锁的申请按照正确的顺序。
-
每当等待有可能导致死锁时,进行事务回滚而不是等待加锁
使用抢占式事务回滚。在抢占机制中,若事务Tj所申请的锁己被事务Ti持有,则授予Ti的锁可能通过回滚事务Ti被抢占(preempted),并将锁授予Tj。为控制抢占,我们给每个事务赋一个时间戳,系统仅用时间戳来决定事务应当等待还是回滚。并发控制仍使用封锁机制。若一个事务回滚,则该事务重启时保持原有的时间戳。已提出利用时间戳的两种不同的死锁预防机制:
- Wait-die机制基于非抢占技术。当事务Ti申请的数据项当前被Tj持有,仅当Ti的时间戳小于Tj的时间戳(即Ti比Tj老)时,允许Ti等待。否则,Ti回滚(死亡)。要申请的锁被年轻事务持有,等待。被老事务持有,回滚(放弃)自己。
- Wound-wait机制基于抢占技术,是与wait-die相反的机制。当事务Ti申请的数据项当前被Tj持有,仅当Ti的时间戳大于Tj的时间戳(即,Ti比Tj年轻)时,允许Ti等待。否则,Tj回滚(Tj被Ti伤害)。要申请的锁被年轻事务(更后开始的事务)持有,将年轻事务回滚。
基于锁超时(lock timeout)。申请锁的事务至多等待一段给定的时间。若在此期间内未授予该事务锁,则称该事务超时,此时该事务自己回滚并重启。如果确实存在死锁,卷入死锁的一个或多个事务将超时并回滚,允许其他事务继续。该机制介于死锁预防(不会发生死锁)与死锁检测与恢复之间。
超时机制的实现极其容易,并且如果事务是短事务并且长时间等待很可能由死锁引起的,该机制动作良好。然而,一般而言很难确定一个事务超时之前应等待多长时间。如果已发生死锁,等待时间太长导致不必要的延迟。如果等待时间太短,即便没有死锁,也可能引起事务回滚,造成资源浪费。该机制也可能会产生饿死。因此,基于超时的机制应用有限。
总结:
- 如果能提前知道调度中所有事务要访问的数据项,则简单。只需要求加锁按一定的顺序即可;
- 通常都无法提交知道所有事务要访问的数据项,则:
- 非抢占:要申请的锁被老事务持有,回滚自己;被年轻的事务持有,等待年轻的事务;
- 抢占:要申请的锁老事务持有,等待;被年轻的事务持有,将年轻的事务回滚。
- 等待申请锁过久,将自己回滚重启。
死锁检测与恢复
死锁检测
利用有向图描述事务之间的等待关系。G=(V,E), V:事务,Ti->Tj表示事务Ti在等待Tj释放所需数据项。当图中存在环时,系统存在死锁。系统维护等待图,并周期性地激活在等待图中搜索环的算法。何时激活算法:
- 死锁发生的频度?
- 有多少事务将受到死锁的影响?
如果死锁频繁发生,则检测算法应比通常激活得更频繁。分配给处于死锁中事务的数据项在死锁解除之前不能被其他事务获取。此外,等待图中环的数目可能增大。在最坏的情况下,我们要在每个分配请求不能被满足时激活检测算法。
死锁恢复
回滚一个或多个事务
- 选择牺牲者:“最小代价”
- 事务已经计算了多久,并在完成指定任务之前事务还将计算多久时间?
- 该事务已经使用了多少数据项?
- 为完成事务,该事务还需使得多少数据项?
- 回滚时将牵扯到多少事务
- 回滚
- 彻底回滚:中止事务,重新开始
- 部分回滚:要求事务维护所有正在运行事务的额外状态信息。确切地说,需要记录锁的申请/授予序列和事务执行的更新。死锁检测机制应确定,为打破死锁,选定的事务需要释放哪些锁。选定的事务必须回滚到获得这些锁的第一个之前,并取消它在此之后的所有动作。恢复机制必须能够处理这些回滚。而且,事务必须能够在部分回滚之后恢复执行。
- 饿死:有可能同一事务总是被选为牺牲者,导致该事务饿死。保证一个事务被选为牺牲者的次数有限。在代价因素中加入回滚次数。