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

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

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

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

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

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

数据库并发的几大类问题

①丢失修改(Lost Update)

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

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

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

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

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

③幻读(Phantom Read)

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

④读脏数据(Dirty Read)

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

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

基本封锁类型

①排它锁(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做任何修改。

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

封锁协议

在运用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的更新。

二级封锁协议

二级封锁协议:一级封锁协议加上事务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。这样就避免了读“脏”数据。

三级封锁协议

三级封锁协议:一级封锁协议加上事务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锁。这样就避免了不可重复读。

活锁和死锁

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

活锁

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

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

死锁

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

死锁的预防

①一次封锁法

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

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

②顺序封锁法

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

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

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

死锁的诊断与解除

①超时法

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

②等待图法

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

两段锁协议

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

  • 在对任何数据进行读、写操作之前,首先要申请并获得该数据的封锁。
  • 在释放一个封锁之后,事务不在申请和获得其他封锁。

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

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

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

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

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

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

数据库隔离级别

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

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

原文发布于微信公众号 - 撸码那些事(lumanxs)

原文发表时间:2018-05-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏老司机的技术博客

宝宝都能学会的python编程教程7:元祖(tuple)

元祖 元祖看起来和列表很相似,但是不包括方括号,因此,一般这样定义和使用元祖: >>> tuple=1,2,3 >>> tuple (1, 2, 3) >>> ...

3068
来自专栏较真的前端

看到这题后还敢说自己精通Promise吗?

1422
来自专栏java一日一条

Java 8新的时间日期库的20个使用示例

除了lambda表达式,stream以及几个小的改进之外,Java 8还引入了一套全新的时间日期API,在本篇教程中我们将通过几个简单的任务示例来学习如何使用J...

502
来自专栏Java帮帮-微信公众号-技术文章全总结

Java并发编程,一定要有自己的理解【面试+工作】

编写优质的并发代码是一件难度极高的事情。Java语言从第一版本开始内置了对多线程的支持,这一点在当年是非常了不起的,但是当我们对并发编程有了更深刻的认识和更多的...

987
来自专栏用户画像

3.1.4.1 基本分页存储管理方式

非连续分配允许一个程序分散地装入到不相邻的内存分区中,根据分区的大小是否固定分为分页存储管理方式和分段存储管理方式。

501
来自专栏青玉伏案

ReactiveSwift源码解析(十一) Atomic的代码实现以及其中的Defer延迟、Posix互斥锁、递归锁

本篇博客我们来聊一下ReactiveSwift中的原子性操作,在此内容上我们简单的聊一下Posix互斥锁以及递归锁的概念以及使用场景。然后再聊一下Atomic的...

3215
来自专栏IMWeb前端团队

ES6解构嵌套对象

本文作者:IMWeb zzbozheng 原文出处:IMWeb社区 未经同意,禁止转载 让我们先回忆一下ES6的对象解构,本文介绍各种ES6的对象解构...

1885
来自专栏知识分享

1-关于单片机通信数据传输(中断发送,大小端,IEEE754浮点型格式,共用体,空闲中断,环形队列)

写这篇文章的目的呢,如题目所言,我承认自己是一个程序猿.....应该说很多很多学单片机的对于...先不说别的了,,无论是学51的还是32的,,,先问一下大家用串...

2465
来自专栏Golang语言社区

go语言数据结构 环形队列

队列是一种常用的数据结构,这种结构保证了数据是按照“先进先出”的原则进行操作的,即最先进去的元素也是最先出来的元素.环形队列是一种特殊的队列结构,保证了元素也是...

852
来自专栏种道伟的专栏

Lua 性能剖析

lua语言在游戏行业大受欢迎,因运行效率高(相比于其他脚本语言),热更方便等原因被广泛应用。想在现有c++系统中引入lua,被挑战的第一个问题往往是:“Lua性...

3.4K3

扫码关注云+社区