并发控制概述
多事务串行执行方式
- 事务串行执行
- 每个时刻只有一个事务运行,其他事务必须等到这个事务结束以后才能运行
- 不能充分利用系统资源发挥数据库共享资源的特点
- 交叉并发执行
- 事务的并行执行是这些并行事务的并行操作轮流交叉运行
- 是单处理机系统中的并发方式,能够减少处理机的空闲时间,提高系统的效率
- 同时并发方式
- 多处理机系统中,每个处理机可以运行一个事务,多个处理机同时运行多个事务,实现多个事务真正的并行运行
- 最理想的并发方式,但受制于硬件环境
- 更复杂的并发方式机制
事务并发执行带来的问题
可能会存取和存储不正确的数据,破坏事务的隔离性和数据的一致性
DMBS 必须提供并发控制机制
并发控制机制是衡量一个DBMS性能的重要标志之一
并发控制机制的任务
- 对并发操作进行正确调度
- 保证事务的隔离性
- 保证数据库的一致性
数据不一致
-
丢失修改
- 多个事务从数据库中读入同一个数据并修改,某个事务的提交结果破坏了另外一个事务提交的修改,导致该事务的修改丢失
-
可重复读
-
事务1执行读取数据操作后,事务2执行更新操作,使事务1无法再现前一次读取结果
-
事务1读取某一数据后(三类不可重复读)
-
事务2对其做了修改,事务1再次读该数据,得到与前一次不同的值
-
事务2删除了其中部分记录,事务1再次读数据,发现某些数据消失了
-
事务2插入部分部分记录,事务1再次读数据,发现多了一些数据
- 后两种情况也称为幻影现象
-
-
读“脏”数据
- 事务1修改某一磁盘,并将其修改回磁盘,事务2读取同一数据,但由于某种原因事务1被撤销,这时事务1已修改的数据恢复为原来值,事务2读到的数与原数据库不一致,是不正确的数据,又称为脏数据
封锁
什么是封锁?
封锁就是事务T在对某个数据对象操作之前,先向系统发出请求,对其加锁
加锁后事务T就对该数据对象有了一定的控制,在事务T释放它的锁之前,其他事务不能更新此数据对象
封锁是实现并发控制的一个非常重要的技术
基本封锁类型
排他锁 写锁
事务T 对数据对象A加上 X锁,只允许该事务读取和修改 A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。
保证了其他事务在T释放A上的锁之前不能再读取和修改A
共享锁 读锁
事务T 对数据对象A加上S 锁,则事务T可以读A 但不能修改A,其他事务能对A加 S 锁,但不能加 X 锁,直到 T 释放A上的S锁。
保证了其他事务可以读A,但在T释放A上的S锁前不能对A做任何修改
排他锁与共享锁的控制方式 可以 用相容矩阵 来表示
T1\T2 | X | S | - |
---|---|---|---|
X | N | N | Y |
S | N | Y | Y |
- | Y | Y | Y |
封锁机制 解决 数据不一致问题
封锁协议
一句话理解就是,什么时候加锁,什么时候释放锁
-
一级封锁协议是:事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。
- 一级封锁协议可以防止丢失修改,并保证事务T是可恢复的。使用一级封锁协议可以解决丢失修改问题。
在一级封锁协议中,如果仅仅是读数据不对其进行修改,是不需要加锁的,它不能保证可重复读和不读“脏”数据。
- 一级封锁协议可以防止丢失修改,并保证事务T是可恢复的。使用一级封锁协议可以解决丢失修改问题。
-
二级封锁协议是:一级封锁协议加上事务T在读取数据R之前必须先对其加S锁,读完后方可释放S锁。
- 二级封锁协议除防止了丢失修改,还可以进一步防止读“脏”数据。但在二级封锁协议中,由于读完数据后即可释放S锁,所以它不能保证可重复读。
-
三级封锁协议是:一级封锁协议加上事务T在读取数据R之前必须先对其加S锁,直到事务结束才释放。
- 三级封锁协议除防止了丢失修改和不读“脏”数据外,还进一步防止了不可重复读。
上述三级协议的主要区别在于什么操作需要申请封锁,以及何时释放。
封锁协议可以有效解决并行操作的一致性问题,但也带来一些新的问题
活锁和死锁
活锁
处理要求得不到响应,多个事务对同一个对象进行操作,但事务可能很多,最后一个请求的要求一直得不到满足,
指某个事物永远处于等待状态,得不到执行的现象
避免活锁
- 采用先来先服务策略
死锁
由于事务的推进顺序不协调,导致资源无法
死锁的四个必要条件
- 互斥
- 不可剥夺
- 请求保持
- 环路等待
解决死锁
- 预防死锁(破坏产生死锁的四个必要条件)
- 一次封锁法
- 将一次事务需要的所有数据全部加锁 (破坏请求保持条件),
- 粒度大,降低了系统并发度,无法精确确定要封锁的对象
- 顺序封锁法
- 对数据对象规定一个封锁顺序,所有事务都按这个顺序实行封锁(破坏 环路等待条件)
- 维护成本高,难于实现
- 死锁的诊断与解除(DBMS 使用更为普遍)
- 允许死锁发生
- 解除死锁
- 由并发子系统定期检测系统中是否存在 死锁
- 检测死锁
- 超时法
- 一个事务等待时间超过规定的时限,认为发生死锁
- 等待图法(图中出现回路,表示系统出现了 死锁)
- 解除死锁
- 选择一个处理死锁代价最小的事务,将其撤销,释放此事务持有的锁,使其他事务继续运行下去
并发调度的可串行性
理解,不加控制的并行操作得到的结果是不一致的,但如果 某次并行执行的结果 与 按照某种次序 串行执行他们结果相同, 那么此次 并行的执行顺序(或者称为调度策略)称为 可串行化的调度(即是用来形容一次 并行调度的)
可串行化调度
计算机系统对并行事务中并行操作的调度是随机的,而不同的调度可能会产生不同的结果
采用封锁,可以解决并行调度不一致性问题
可串行性是并行事务正确性的唯一准则
冲突可串行化调度
冲突操作:
不同事务对同一个数据的
-
读写操作
-
写写操作
??? 不同事务的冲突操作和同一事务的两个操作是不能交换的
一个调度Sc在保证冲突操作的次序不变的情况下,通过交换两个事务不冲突操作的次序,得到另一个调度Sc’,如果Sc'是串行的,称调度Sc为 冲突可串行化调度
一个调度是冲突可串行化,一定是可串行化的调度
冲突可串行化调度 是可串行化调度的充分条件,不是必要条件
思考? 并发控制的常用技术是封锁,那么如何使封锁机制能够产生可串行化调度呢?
两段锁协议
两段锁协议 2PL ========== >实现并发调度的可串行性,从而保证调度正确性
封锁协议
- 何时申请加锁
- 持锁时间、何时释放锁
两段锁协议是最常用的一种封锁协议,理论上已证明使用它产生的是可串行化调度
两段锁协议的内容
在对任何数据进行读、写操作之前,事务首先要获得对该数据的封锁
在释放一个封锁之后,事务不再申请和获得任何其他封锁
两段锁的含义
事务分为两个阶段
- 第一阶段是获得封锁,也称为 扩展阶段
- 第二阶段是释放封锁,也称为 收缩阶段
若并行执行的所有事务都是遵守两段锁协议,则这些事务的所有并行调度策略都是 可串行化调度
所有遵守两段锁协议的事务,其并行执行的结果一定是正确的
事务遵守两段锁协议是可串行化调度的充分条件,不是必要条件
可串行化调度中,不一定所有事务都必须符合两段锁协议
封锁的粒度
封锁粒度
封锁对象的大小称为 封锁的粒度
封锁的对象:
- 逻辑单元
- 属性值、属性值集合、元组、关系、索引项、整个索引、整个数据库
- 物理单元
- 页(数据页或索引页)、物理记录等
封锁粒度与系统的并发度和并发控制的开销成反比
多粒度封锁
在一个系统中同时支持多种封锁粒度,供不同的事务选择
(这里想到了mysql的引擎Innodb 封锁粒度是 一条记录 是一张表)
-
多个关系的大量元组,以 数据库为封锁单位
-
大量元组的用户事务,以关系为封锁单位
-
少量元组的用户事务,以元组为封锁单位
多粒度树
以树形结构表示多级封锁粒度
根结点是 整个数据库表示最大数据粒度
叶结点表示最小的数据粒度
多粒度封锁协议
-
允许多粒度树中每个结点被独立地加锁
-
对一个结点加锁,也意味着对所有子节点也加同样类型的锁
-
多粒度封锁中,一个数据对象可能以两种方式封锁
- 显式封锁
- 隐式封锁
对某个数据对象对象加锁时系统检查的内容
该数据对象
- 有无显式封锁与之冲突
所有上级结点
- 检查本事务的显式封锁是否与该数据对象上的隐式封锁冲突(由上一级结点封锁造成)
所有下级结点
- 看上面的显式封锁是否与本事务的隐式封锁(将加到下级结点的封锁)冲突
意向锁
引入意向锁的目的:加快对某个数据对象加锁时系统的检查效率
含义
如果对任何一个结点加基本锁,必须先对它的上层结点加 意向锁,
如果对一个结点加意向锁,则说明该节点的下层节点正在被加锁
例
对某个元组r加锁,先对关系R加意向锁
- 事务T要对关系R加X锁,系统只要检查根结点数据库和关系R是否已加了不相容的锁
- 不需要搜索和检查R中的每一个元组是否加了X锁
常用意向锁
-
意向共享锁 IS锁
-
意向排他锁 IX锁
-
共享意向排他锁 SIX锁
意向锁相容矩阵
T1\T2 | S | X | IS | IX | SIX | - |
---|---|---|---|---|---|---|
S | Y | N | Y | N | N | Y |
X | N | N | N | N | N | Y |
IS | Y | N | Y | Y | Y | Y |
IX | N | N | Y | Y | N | Y |
SIX | N | N | Y | N | N | Y |
- | Y | Y | Y | Y | Y | Y |
锁的强度
锁的强度是指它对其他锁的排斥程度
一个事务在申请封锁时,以强锁代替弱锁是安全的,反之不然
具有意向锁的多粒度封锁方法
- 申请封锁时应该按自上而下次序进行
- 释放封锁时应该按自下而上次序进行
对一个数据加锁,要先对它上层节点加意向锁