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

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

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

【眼见为实】本身动手实践理解READ UNCOMMITED && SERIALIZABLE微信

【眼见为实】本身动手实践理解 READ COMMITTED && MVCC并发

【眼见为实】本身动手实践理解REPEATABLE READ && Next-Key Lock学习

数据库并发的几大类问题

①丢失修改(Lost Update)

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

mark

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

mark

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

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

mark

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

##③幻读(Phantom Read)blog

幻读实际上是不可重复读的一种特殊状况。不可重复读(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
左侧T一、T2遵循两段锁协议,右侧T一、T2并不遵循两段锁协议

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

一次封锁法要求事务必须将要使用的数据所有加锁,不然不能继续执行。所以一次封锁法遵照两段锁协议。

可是两段锁协议并不要求事务将要使用的数据一次所有加锁,所以两段锁协议可能发生死锁。如图:

mark

数据库隔离级别

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

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

mark


做者: 撸码那些事
声明:本文为博主学习感悟总结,水平有限,若是不当,欢迎指正。若是您认为还不错,不妨关注一下下方的 微信公众号,谢谢支持。转载与引用请注明出处。
相关文章
相关标签/搜索