【眼见为实】数据库并发问题 封锁协议 隔离级别

此篇博客是【眼见为实】系列的第一篇博客,主要从理论上讲了数据库并发可能会出现的问题,解决并发问题的技术——封锁,封锁约定的规则——封锁协议。然后简单说明了数据库事务隔离级别和封锁协议的对应关系。后面的几篇博客都是通过亲身实践探究InnoDB引擎在各个隔离级别下的实现细节。

【眼见为实】数据库并发问题 封锁协议 隔离级别

【眼见为实】自己动手实践理解READ UNCOMMITED && SERIALIZABLE

【眼见为实】自己动手实践理解 READ COMMITTED && MVCC

【眼见为实】自己动手实践理解REPEATABLE READ && Next-Key Lock

数据库并发的几大类问题

①丢失修改(Lost Update)

两个事务T1和T2同时读入同一数据并修改,T2的提交的结果破坏了T1提交的结果,导致T1的修改被丢失(第二类丢失更新)。
mark

还有一种特殊的丢失修改(第一类丢失更新),如下图。因为这种丢失修改在【READ UNCOMMITED】隔离级别下都不会出现,所以不进行讨论。

mark

②不可重复读(Non-Repeatable Read)

事务T1读取数据后,事务T2执行更新操作,使事务T1无法再现前一次读取结果。
具体包括三种情况:
(1)事务T1读取某一数据后,事务T2对其做了修改,当事务T1再次读取该数据时,得到与前一次不同的值。

mark

(2)事务T1按照一定条件读取了某些数据记录后,事务T2删掉了其中部分记录,当T1再次按相同条件查询数据时,发现某些记录消失了。
(3)事务T1按照一定条件读取了某些数据记录后,事务T2插入了一些记录,当T1再次按相同条件查询数据时,发现多了一些记录。

③幻读(Phantom Read)

幻读其实是不可重复读的一种特殊情况。不可重复读(2)和(3)也称为幻读现象。不可重复读是对数据的修改更新产生的;而幻读是插入或删除数据产生的。

mark

④读脏数据(Dirty Read)

事务T1修改某一数据,并将其写回磁盘,事务T2读取同一数据后,T1因为某些原因回滚,这时T1修改过的数据恢复原值,T2读取到的数据就与数据库中的数据不一致,则T2读取到数据就为“脏数据“,即不正确的数据。
mark

并发控制的主要技术是封锁

基本封锁类型
①排它锁(Exclusive Locks,简称X锁)
排它锁又称为写锁。若事务T对数据对象A加上X锁,则只允许T修改和读取A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其他事务在T释放A上的锁之前都不能再读取和修改A。
②共享锁(Share Locks,简称S锁)
共享锁又称为读锁。若事务T对数据对象A加上S锁,则事务T可以读取A但不能修改A。其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其他事务可以读取A,但是在T释放A上的S锁之前不能对A做任何修改。

排它锁与共享锁的相容矩阵

mark

封锁协议

在运用X锁和S锁这两种基本封锁,对数据对象加锁时,还需要约定一些规则。例如何时申请X锁和S锁,持锁时间,何时释放等。这些规格称为封锁协议。

一级封锁协议

一级封锁协议:事务T在修改数据A之前必须对其加X锁,直到事务结束才释放。事务结束包括正常结束(Commit)和非正常结束(RollBack)
一级封锁协议可防止丢失修改
使用一级封锁协议解决了图1中的覆盖丢失问题。事务T1在读A进行修改之前先对A加X锁,当T2再请求对A加X锁时被拒绝,T2只能等待T1释放A上的锁后T2获得A上的X锁,这时它读取的A已经是T1修改后的15,再按照此值进行计算,将结果值A=14写入磁盘。这样就避免了丢失T1的更新。

mark

二级封锁协议

二级封锁协议:一级封锁协议加上事务T在读取数据A之前必须先对其加S锁,读完后即可释放S锁
二级封锁协议除防止了丢失修改,还进一步防止了读“脏”数据
使用二级封锁协议解决了图2中的脏读问题。事务T1在读C进行修改之前先对C加X锁,修改其值后写回磁盘。这时T2请求在C上加S锁,因为T1在C上已经加了X锁,所以T2只能等待。T1因为某种原因被撤销,C恢复原值100。T1释放C上的X锁后T2获得C上的S锁,读C=100。这样就避免了读“脏”数据。
mark

三级封锁协议

三级封锁协议:一级封锁协议加上事务T在读取数据A之前必须先对其加S锁,直到事务结束才释放
三级封锁协议除防止了丢失修改和读“脏”数据,还进一步防止了不可重复读
使用三级封锁协议解决了图3中的不可重复读问题。事务T1在读取数据A和数据B之前对其加S锁,其他事务只能再对A、B加S锁,不能加X锁,这样其他事务只能读取A、B,而不能更改A、B。这时T2请求在B上加X锁,因为T1已经在B上加了S锁,所以T2只能等待。T1为了验算结果再次读取A、B的值,因为其他事务无法修改A、B的值,所以结果仍然为150,即可重复读。此时T1释放A、B上的S锁,T2才获得B上的X锁。这样就避免了不可重复读。
mark

活锁和死锁

封锁可能会引起活锁活死锁。

活锁

如果事务T1封锁了数据R,事务T2又请求封锁数据R,于是T2等待。事务T3也请求封锁R,当事务T1释放了数据R上的封锁之后系统首先批准了事务T3的封锁请求,T2仍然等待。然后T4又申请封锁R,当T3释放了R的封锁之后系统又批准了T4的封锁请求。T2有可能一直等待下去,这就是活锁。

mark

避免活锁的方法就是先来先服务的策略。当多个事务请求对同一数据对象封锁时,封锁子系统按照请求的先后对事务排队。数据对象上的锁一旦释放就批准申请队列中的第一个事务获得锁。

死锁

如果事务T1封锁了数据R1,事务T2封锁了数据R2,然后T1又请求封锁数据R2,因为T2已经封锁了数据R2,于是T1等待T2释放R2上的锁。接着T2又申请封锁R1,因为因为T1已经封锁了数据R1,T2也只能等待T1释放R1上的锁。这样就出现了T1在等待T2,T2也在等待T1的局面,T1和T2两个事务永远不能结束,形成死锁。

mark

死锁的预防

①一次封锁法

一次封锁法要求事务必须一次将所有要使用的数据全部加锁,否则不能继续执行。例如上图中的事务T1将数据R1和R2一次加锁,T1就能执行下去,而T2等待。T1执行完成之后释放R1,R2上的锁,T2继续执行。这样就不会产生死锁。

一次封锁法虽然能防止死锁的发生,但是缺点却很明显。一次性将以后要用到的数据加锁,势必扩大了封锁的范围 ,从而降低了系统的并发度。

②顺序封锁法

顺序封锁法是预先对数据对象规定一个封锁顺序,所有的事务都按照这个顺序实行封锁。

顺序封锁法虽然可以有效避免死锁,但是问题也很明显。第一,数据库系统封锁的数据对象极多,并且随着数据的插入、删除等操作不断变化,要维护这样的资源的封锁顺序非常困难,成本很高。第二,事务的封锁请求可以随着事务的执行动态的确定,因此很难按照规定的顺序实行封锁。

可见,预防死锁的产生并不是很适合数据库的特点,所以在解决死锁的问题上普遍采用的是诊断并且解除死锁。

死锁的诊断与解除

①超时法

如果一个事务的等待时间超过了默认的时间,就认为是产生了死锁。

②等待图法

一旦检测到系统中存在死锁就要设法解除。通常的解决方法是选择一个处理死锁代价最小的事务,将其撤销,释放此事务持有的所有的锁,恢复其所执行的数据修改操作,使得其他事务得以运行下去。

两段锁协议

所谓的二段锁协议是指所有事务必须分两个阶段对数据进行加锁和解锁操作。

  • 在对任何数据进行读、写操作之前,首先要申请并获得该数据的封锁。

  • 在释放一个封锁之后,事务不在申请和获得其他封锁。

也就是说事务分为两个阶段。第一个阶段是获得封锁,也称为扩展阶段。在这个阶段,事务可以申请获得任何数据项任何类型的锁,但是不能释放任何锁。第二阶段是释放封锁,也称为收缩阶段。在这个阶段,事务可以释放任何数据项上任何类型的封锁,但是不能再申请任何锁。

事务遵守两段锁协议是可串行化调度的充分条件,而不是必要条件。也就是说遵守两段锁协议一定是可串行化调度的,而可串行化调度的不一定是遵守两段锁协议的。

mark
左侧T1、T2遵循两段锁协议,右侧T1、T2并不遵循两段锁协议

两段锁协议和一次封锁法的异同

一次封锁法要求事务必须将要使用的数据全部加锁,否则不能继续执行。因此一次封锁法遵守两段锁协议。

但是两段锁协议并不要求事务将要使用的数据一次全部加锁,因此两段锁协议可能发生死锁。如图:

mark

数据库隔离级别

封锁协议和隔离级别并不是严格对应的

各种隔离级别所能避免的并发问题

mark


本文为博主学习感悟总结,水平有限,如果不当,欢迎指正。

如果您认为还不错,不妨点击一下下方的[【推荐】](javascript:void(0)😉按钮,谢谢支持。

转载与引用请注明出处。

posted @ 2018-03-26 14:47  CoderFocus  阅读(4179)  评论(7编辑  收藏  举报