目录
数据库并发的几大类问题
丢失修改(Lost Update)
不可重复读(Non-Repeatable Read)
幻读(Phantom Read)
读脏数据(Dirty Read)
并发控制的主要技术是封锁
排它锁与共享锁的相容矩阵
封锁协议
一级封锁协议
二级封锁协议
三级封锁协议
活锁和死锁
活锁
死锁
两段锁协议
两段锁协议和一次封锁法的异同
数据库隔离级别
各种隔离级别所能避免的并发问题
序
此篇博客是[眼见为实] 系列的第一篇博客, 主要从理论上讲了数据库并发可能会出现的问题, 解决并发问题的技术 -- 封锁, 封锁约定的规则 -- 封锁协议. 然后简单说明了数据库事务隔离级别和封锁协议的对应关系. 后面的几篇博客都是通过亲身实践探究 InnoDB 引擎在各个隔离级别下的实现细节.
[眼见为实] 数据库并发问题 封锁协议 隔离级别 http://www.cnblogs.com/songwenjie/p/8644674.html
[眼见为实] 自己动手实践理解 READ UNCOMMITED && SERIALIZABLE http://www.cnblogs.com/songwenjie/p/8644599.html
[眼见为实] 自己动手实践理解 READ COMMITTED && MVCC http://www.cnblogs.com/songwenjie/p/8644646.html
[眼见为实] 自己动手实践理解 REPEATABLE READ && Next-Key Lock http://www.cnblogs.com/songwenjie/p/8643684.html
数据库并发的几大类问题
丢失修改(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 并不遵循两段锁协议
两段锁协议和一次封锁法的异同
一次封锁法要求事务必须将要使用的数据全部加锁, 否则不能继续执行. 因此一次封锁法遵守两段锁协议.
但是两段锁协议并不要求事务将要使用的数据一次全部加锁, 因此两段锁协议可能发生死锁. 如图:
数据库隔离级别
封锁协议和隔离级别并不是严格对应的.
各种隔离级别所能避免的并发问题
本文为博主学习感悟总结, 水平有限, 如果不当, 欢迎指正.
如果您认为还不错, 不妨点击一下下方的[推荐] 按钮, 谢谢支持.
来源: http://www.92to.com/bangong/2018/04-13/33563936.html