数据库并发控制协议

全文主要参考数据库系统概念一书以及mooc上战德臣老师的数据库课程
  事务最基本的特性之一是隔离性,当数据库中有多个事务并发执行的时候,隔离性不必定能保持。为了保持事务的隔离性,系统必须对并发事务之间的相互做用加以控制,这是被称为并发控制机制来实现的。本文讲述的机制都是保证调度是可串行化的。最经常使用机制有两阶段封锁快照隔离。html

  关于串行化与一致性的关系:数据库并发控制的基本目标是确保事务的并发执行不会致使数据库一致性的丢失。能够利用可串行性概念来达到这一目标,由于全部可串行化的调度都能保持数据库的一致性。然而,并不是全部保证数据库一致性的调度都是可串行化的。(也存在着容许非可串行化调度的并发控制机制,详见数据库系统概念25章)mysql

1、基于锁的协议

  确保串行化的方法之一就是要求对数据项的访问以互斥的方式进行。也就是说当一个事务访问某个数据项时,其余任何=事务都不能修改该数据项。实现该需求最经常使用的方法是只容许事务访问当前该事务持有该数据项的锁的数据项算法

1.共享锁和排他锁

  给数据项加锁的方式有多种,这一节只考虑两种(这两种也正是MySQL默认引擎(InnoDB)在行级锁定时所使用的)sql

  1. 共享锁:若是事务Ti得到了数据项Q的共享型锁(记为S),则Ti可读但不能写Q。
  2. 排他锁:若是事务Ti得到了数据项Q的排他型锁(记为X),则Ti既可读又可写Q。

       每一个事务都要根据将对数据项Q进行的操做类型申请适当的锁。该请求发送给并发控制管理器,只有并发控制管理器授予所需锁后,事务才能继续其操做。
  相容:假设事务Ti请求对数据项Q加A类型的锁,而事务Tj(i不等于j)已在Q上拥有B类型的锁。若是事务Ti仍能当即得到在数据项Q上A类型的锁,则说A类型的锁和B类型的锁相容。相容矩阵以下所示:
clipboard.png 数据库

  即共享型锁与共享型锁相容,而与排他型锁不相容。在任什么时候候,一个具体的数据项上可能同时有(被不一样的事务持有的)多个共享锁。如事务在访问一数据项时而在数据项上已经存在可不相容类型的锁,那么只能等待该数据上全部不相容的锁被释放才能得到锁从而对数据项进行访问。
  注意:一个事务只要还在访问一个数据项,那么它就必须拥有该数据项上的锁。另外,让事务对一个数据项做最后一次访问后当即释放该数据项上的锁也未必是可取的,由于这可能破坏串行化。以下图例子所示,事务T2看到了不一致的状态(A+B的值),而串行化就是为了事务开始和结束之间的中间状态不会被其余事务看到。因此咱们下面的讨论都是创建在不会在结束访问一个数据项后当即释放锁的状况下的(如两阶段锁协议下的)。这也就会致使了死锁发生的可能性的存在,但死锁能够经过回滚事务来解决,出现死锁比出现不一致状态好得多。
clipboard.png并发

2.死锁与饿死

  加锁可能会出现两个事务都在等待对方解除它所占用数据项上的锁(也多是多个事务之间的循环等待),这种现象称为死锁当死锁发生时,系统必须回滚两个事务中的一个。一旦某个事务回滚,该事务锁住的数据项就被解锁,其余事务就能够访问这些数据项,继续本身的执行例以下图所示就必须回滚:
clipboard.png性能

  饥饿(饿死):当一个事务想要对一个数据项上加排他锁,由于该数据项上已经有其余事务所加的共享锁了,所以必须等待。而在等待期间又有其余事务对该数据项加上了共享锁,以前的那个事务对一段时间后解除了共享锁,但当前事务仍是要继续等待,就这样不断地出现对该数据项加共享锁的其余事务,那么该事务则会一直处于等待状态,永远不可能取得进展,这称为饥饿或者饿死。spa

  避免饿死的方法:合适的并发控制器受权加锁的条件能够规避饿死状况的发生。如,当事务Ti申请对数据项Q加M型锁时,并发控制管理器受权加锁的条件是3d

  • 不存在 已在数据项Q上持有与M型锁冲突的锁 的其余事务
  • 不存在 等待对数据项Q加锁而且先于Ti申请加锁 的其余事务(即按照顺序来)

       在这样的受权加锁条件下,一个加锁请求就不会被气候的加锁申请阻塞。code

3.两阶段锁协议

  保证可串行性的一个协议是两阶段锁协议。该协议要求每一个事务分两个阶段提出加锁和解锁申请:

  1. 增加阶段:事务能够得到锁,但不能释放锁
  2. 缩减阶段:事务能够释放锁,但不能得到锁

  例如事务T3和T4是两阶段的,T1和T2不是两阶段的。两阶段锁协议能够保证冲突可串行化,但没法避免死锁的出现。在两阶段对的事务中最后加锁的位置称为锁点(lock point)。
  两阶段锁协议有两个增强版:严格两阶段锁协议强两阶段锁协议

  1. 严格两阶段锁协议:在两阶段锁协议的基础上,还要求事务持有的全部排他锁必须在事务提交以后方可释放
  2. 强两阶段锁协议:在两阶段协议的基础上,要求全部锁都必须在事务提交以后方可释放

2、基于时间戳的协议

  另外一类实现可串行化的技术是为每一个事务分配一个时间戳,这个时间戳一般就是事务的开始的时间。对于每一个数据项,系统维护两个时间戳,一个读时间戳和一个写时间戳。数据项的读时间戳记录该数据项的的事务的最大(即最近的)时间戳,数据项的写时间戳记录写入该数据项当前值的事务的时间戳。时间戳用来确保在访问冲突状况下,事务按照时间戳的顺序来访问数据项。当按照时间戳的顺序,一事务不能访问时,该事务会被停止,而且分配一个新的时间戳从新开始。

时间戳的实现机制有两种:

  1. 使用系统时钟的值做为时间戳,即事务的时间戳等于该事务进入系统这里包括后面的系统一词指的都是数据库系统时的时钟值。
  2. 使用逻辑计数器,每赋予一个时间戳,计数器增长计数,即事务的时间戳等于该事务进入系统时的计数器值。

       

时间戳的访问顺序

  1. 对于一个进行读取数据项的事务,只有在当前事务的时间戳大于等于数据项上的写时间戳时,才能进行读取,并将数据项的读时间戳更新为当前时间戳
  2. 对于一个进行写入数据项的事务,只有在当前事务的时间戳大于等于数据项上的读时间戳以及写时间戳,才能进行写入操做,并将数据项的写时间戳更新为当前时间戳
  3. 当事务不能知足时间戳顺序要求进行访问操做时,则事务会被回滚,由系统赋予它新的时间戳并从新启动。

时间戳特色

  时间戳协议和两段锁协议类似,都是保证了冲突可串行化,但二者都是保证的冲突可串行化的两个不一样的真子集,存在知足两阶段锁协议却不能知足时间戳协议的调度,反之亦然。时间戳协议保证冲突可串行化的缘由在于:冲突操做是按时间戳顺序进行处理的。
  死锁:时间戳协议保证了无死锁,由于不存在等待的事务。
  饿死:当一系列的短事务引发长事务反复重启时,可能致使长事务饿死的现象。解决方式:若是发现一个事务反复重启,与之冲突的事务应当暂时阻塞,以使该事务可以完成。

3、多版本和快照隔离

  经过维护数据项的多个版本,一个事务容许读取一个旧版本的数据项,而不是被另外一个未提交或者在串行化序列中应该排在后面的事务写入的新版本的数据项。有许多多版本并发控制技术,其中一个是实际中普遍应用的称为快照隔离的技术。
  在快照隔离中,咱们能够视为每一个事务开始时都有其自身的数据库版本或者快照(实际实现中不会复制整个数据库,只有被改变的数据项才会保留多个版本)。事务从这个私有版本中读取数据,所以和其余事务所作的更新隔离开,若是事务更新数据库,更新只出如今其私有版本中,而不是实际的数据库自己中。当事务提交时,和更新有关的信息将保存,使得更新被写入真正的数据库。
  当一个事务T进入提交状态后,只有在 没有其余并发事务已经修改该事务想要更新的数据项 的状况下,事务进入提交状态。而不能提交的事务则终止。
  快照隔离能够保证读数据的尝试永远无需等待(不像锁协议那样要读的数据项上面被加了排他锁就只能等待)。只读事务不会终止。只有修改数据的事务才有微小的终止风险。因为每一个事务读取它本身的数据库版本或快照,所以读数据不会致使此后其余事务的更新尝试被迫等待(不像锁协议要写的数据项中被加了共享锁后就只能等待甚至有饿死的状况)。由于大部分事务是只读的,而且大多数其余事务读数据的状况多于更新,因此这是与锁相比每每能带来性能改善的主要缘由。
  矛盾在于,快照隔离带来的问题是它提供了太多的隔离。考虑两个事务T和T',在一个串行化调度中,要么T看到T'所作的全部更新,要么T'看到T所作的全部更新,由于在串行化顺序中一个必须在另外一个以后。在快照隔离下,任何事务都不能看到对方的更新,这是在串行化调度中不会出现的。在许多(事实上,大多数)状况下,两个事务的数据访问不会冲突,所以没有什么问题。DNA一旦T读取的是T'要更新的数据项,T'读取的是T'要更新的数据项,则可能两个事务都没法读取到对方的更新。结果可能致使数据库的不一致状态。
  详情参看数据库系统概念第六版中的15.5基于有效性检查的协议、15.6多版本机制、15.7快照隔离。
  注意:根据论文,快照隔离能排除掉严格版本的幻读A3,但对于宽泛版本的P3(实际上使得隔离性更严格了)是不能排出的,此外还存在写偏斜(write skew)的异常。

4、死锁的处理

  若是存在一个事务集,该集合中的每一个事务都在等待该集合中的另外一个事务,那么咱们说系统处于死锁状态。更确切地说,存在一个等待事务集{T0,T1,...,Tn},使得T0正等待被T1锁住的数据项,T1正等待被T2锁住的数据项,....,Tn-1正等待被Tn锁住的数据项,且Tn正等待被T0锁住的数据项。在这种状况下,没有一个事务可以取得进展。
  此时系统的惟一补救措施就是采起激烈的动做,如回滚某些陷于死锁的事务。事务有可能只部分回滚,即事务回滚到 它获得锁的点 以前,这样就释放了锁从而解决死锁。
  处理死锁有两种主要的方法:咱们可使用死锁预防协议来保证系统永不进入死锁状态。另外一种方法是,咱们容许系统进入死锁状态,而后试着用死锁检测死锁恢复机制进行恢复。两种方法均有可能引发事务回滚。若是事务进入死锁状态几率相对比较高,则一般使用死锁预防机制,不然使用检测与恢复机制。
  注意:检测与恢复机制所带来的各类开销,不只包括在运行时维护必要信息及执行检测算法的代价,还要包括从死锁中恢复所固有的潜在损失。

1.死锁预防

  预防死锁主要有两种思路:

  1. 第一种思路是:经过对加锁请求进行排序(顺序封锁法) 要求同时得到全部的锁从而来保证不会发生循环等待(一次封锁法)(就不会发生占着一个数据项的锁还在等着另外一个数据项的锁的状况,要么就一块儿占着要么就都不占)

       一次封锁法就是要求每一个事务在开始以前封锁它的全部的数据项,此外,要么一次所有封锁,要么全不封锁。
  一次封锁协议主要的缺点是(1)在事务开始前一般很难预知哪些数据项须要封锁。(2)数据项的使用率可能很低,由于不少数据项可能封锁很长时间却用不到。
  顺序封锁法是对全部的数据项强加一个次序,同时要求按次序规定的顺序对数据项进行加锁。
  顺序封锁法存在的问题维护成本高。数据库系统中可封锁的数据对象极其众多,而且随数据的插入、删除等操做而不断地变化,要维护这样极多并且变化的资源的封锁顺序很是困难,成本很高。

  2. 第二种思路比较接近死锁恢复,每当等待有可能致使死锁时,进行事务回滚而不是等待加锁
       这种思路的实现方式是使用抢占和事务回滚。在抢占机制中,若事务Tj所申请的锁已经被事务Ti所持有,则授予Ti的锁可能经过回滚事务Ti被抢占,并将锁授予Tj。为控制抢占,咱们给每一个事务赋予一个惟一的时间戳,系统仅用时间戳来决定事务应当等待仍是回滚。并发协议仍使用封锁协议,若一个事务回滚,则该事务重启时保持原有的时间戳
  根据此思路提出的两种相反的预防机制:

  • wait-die机制:当事务Ti申请的数据项被Tj持有,仅当Ti的时间戳小于Tj的时间戳时,容许Ti等待。不然Ti回滚。即如老可等
  • wound-wait机制:当事务Ti申请的数据项被Tj持有,仅当Ti的时间戳大于Tj的时间戳时,容许Ti等待。不然Tj回滚。(Tj被Ti伤害)即如少可等,如老伤少

       两种机制的缺点都是:可能会发生没必要要的回滚。

  3. 还有一种额外的思路是锁超时机制,申请锁的事务至多等待一段给定的时间。若此时间内为授予该事务锁,则称该事务超时,则该事务会进行回滚并重启。该机制介于死锁预防(不会发生死锁)和死锁检与恢复之间。
  锁超时机制实现起来及其容易,但其缺点在于很难肯定一个事务超时以前应等待多长时间,过长则会在死锁时形成延迟,太短则会形成没必要要的回滚。因此仅应用于事务主要是短事务而且长时间的等待很可能是死锁形成的状况下。

2.死锁恢复挖坑待填

相关文章
相关标签/搜索