第十八章 并发控制
事务并发执行
- 可能会存取和存储不正确的数据,破坏事务的隔离性和数据库的一致性
- DBMS 必须提供并发控制机制
- 并发控制机制是衡量一个DBMS性能的重要标志之一
- 本章论述的并发控制机制全部是基于可串行化的
- 并发控制机制的任务
- 对并发操作进行正确调度
- 保证事务的隔离性
- 保证数据库的一致性
基于锁的协议
- 确保可串行化的方法之一是要求对数据项的访问以互斥的方式进行
- 封锁就是事务T在对某个数据对象(例如表、记录等)操作之前,先向系统发出请求,对其加锁
- 加锁后事务T就对该数据对象有了一定的控制,在事务T释放它的锁之前,其它的事务不能更新此数据对象。
- 封锁是实现并发控制的一个非常重要的技术
- 事务结束(以提交的方式结束或者以回滚的方式结束,均需要释放资源,包括释放封锁)
锁
- DBMS通常提供了多种类型的封锁。一个事务对某个数据对象加锁后究竟拥有什么样的控制是由封锁的类型决定的。
基本封锁类型
- 排它锁(eXclusive lock,简记为X锁,写锁)
- 共享锁(Share lock,简记为S锁,读锁)
共享锁/排它锁
- 共享锁又称为读锁
若事务T对数据对象Q加上S锁,事务T可读但不能写Q,其它事务只能再对Q加S锁,而不能加X锁,直到T释放Q上的S锁 - 排它锁又称为写锁
若事务T对数据对象Q加上X锁,则事务T既可以读又可以写Q,其它任何事务都不能再对Q加任何类型的锁,直到T释放Q上的锁
锁的相容矩阵
活锁(饿死)
- 饥饿/饿死
不断出现的申请并获得S锁的事务,使申请X锁的事务一直处在等待状态; - 饥饿的防止
对申请S锁的事务,如果有先于该事务且等待的加X锁的事务,令申请S锁的事务等待
封锁协议
- 在运用X锁和S锁对数据对象加锁时,需要约定一些规则:封锁协议(Locking Protocol)
何时申请X锁或S锁
持锁时间、何时释放
- 不同的封锁协议,在不同的程度上为并发操作的正确调度提供一定的保证
- 封锁协议限制了可能的调度数目,这些调度组成的集合是所有可能的可串行化调度一个真子集
两阶段封锁协议(疑惑)
- 保证可串行性的封锁协议
- 定义:每个事务分两个阶段提出加锁和解锁申请
增长阶段(growing phase):事务可以获得锁,但不能释放锁
缩减阶段(shrinking phase) :事务可以释放锁,但不能获得新锁
- 封锁点(lock point):
事务最后加锁的位置,称为事务的封锁点, 记作Lp(T) - 并行执行的所有事务均遵守两段锁协议,则对这些事务的所有并行调度策略都是可串行化的。
- 所有遵守两段锁协议的事务,其并行执行的结果一定是正确的
- 事务遵守两段锁协议是可串行化调度的充分条件,而不是必要条件
- 可串行化的调度中,不一定所有事务都必须符合两段锁协议。
- 两阶段封锁协议不保证不会发生死锁
- 两阶段封锁协议下,级联回滚可能发生
严格两阶段封锁协议
- 严格两阶段封锁协议除了要求封锁是两阶段之外,还要求事务持有的所有排他锁必须在事务结束后,方可释放。
强两阶段封锁协议
-
事务提交之前,不得释放任何锁
-
在强两阶段封锁协议下,事务可以按其结束的顺序串行化
转换锁
-
Upgrade:从共享锁提升为排他锁
-
Downgrade:从排他锁降级为共享锁
-
锁升级只能发生在增长阶段,锁降级只能发生在缩减阶段
锁实现
- 锁管理器可以作为一个单独的进程来实现
- 事务可以将锁定和解锁请求作为消息发送
- 锁管理器通过发送锁授予消息(或在死锁情况下请求事务回滚的消息)来响应锁请求
- 请求事务等待其请求得到响应
- 锁管理器维护一个称为锁表的内存数据结构,以记录已授予的锁和挂起的请求
基于图的协议
- 树协议确保了冲突的可序列化性以及无死锁。
- 与两阶段锁定协议相比,树锁定协议中的解锁可能更早发生。
- 缩短等待时间,提高并发性
- 协议无死锁,无需回滚
- 缺点
- 协议不保证可恢复性或级联自由
- 需要引入提交依赖项以确保可恢复性
- 事务可能必须锁定它们无法访问的数据项
- 增加了锁定开销和额外的等待时间
- 并发性的潜在降低
- 在两阶段锁定下不可能的调度在树协议下是可能的,反之亦然。
锁实现
死锁处理
从两方面考虑死锁处理
- 预防死锁
- 死锁的诊断与解除
死锁预防
- 产生死锁的原因是存在一个事务集合,其中有事务在等待集合中的另一个事务
- 预防死锁的发生就是要破坏产生死锁的条件
预防死锁的方法
- 一次封锁法和顺序封锁法
- 抢占与事务回滚
一次封锁法
- 要求每个事务必须一次将所有要使用的数据全部加锁,否则就不能继续执行
- 一次封锁法存在的问题:降低并发度
- 扩大封锁范围
- 将以后要用到的全部数据加锁,势必扩大了封锁的范围,从而降低了系统的并发度
- 难于事先精确确定封锁对象
- 数据库中数据是不断变化的,原来不要求封锁的数据,在执行过程中可能会变成封锁对象,所以很难事先精确地确定每个事务所要封锁的数据对象
- 解决方法:将事务在执行过程中可能要封锁的数据对象全部加锁,这就进一步降低了并发度。
顺序封锁法
- 顺序封锁法是预先对数据对象规定一个封锁顺序,所有事务都按这个顺序实行封锁。
- 顺序封锁法存在的问题——维护成本高
- 数据库系统中可封锁的数据对象极其众多,并且随数据的插入、删除等操作而不断地变化,要维护这样极多而且变化的资源的封锁顺序非常困难,成本很高
- 难于实现
- 事务的封锁请求可以随着事务的执行而动态地决定,很难事先确定每一个事务要封锁哪些对象,因此也就很难按规定的顺序去施加封锁。
例:规定数据对象的封锁顺序为A,B,C,D,E。事务T3起初要求封锁数据对象B,C,E,但当它封锁了B,C后,才发现还需要封锁A,这样就破坏了封锁顺序.
抢占与事务回滚
-
在抢占机制中,当事务Ti所申请的锁被事务Tj所持有时,授予Tj的锁可能通过回滚事务Tj被抢占,并将锁授予Ti
-
通过事务的时间戳确定事务等待还是回滚,事务重启时,保持原有的时间戳
-
Wait-die(非抢占技术):当事务Ti申请的数据项当前被事务Tj持有时,仅当Ti的时间戳小于 Tj的时间戳时,允许Ti等待,否则Ti回滚
-
Wound-die (抢占技术) :当事务Ti申请的数据项当前被事务Tj持有时,仅当Ti的时间戳 大于 Tj的时间戳时,允许Ti等待,否则Tj回滚
-
上述两种机制均避免“饿死”:任何时候均存在一个时间戳最小的事务。在这两种机制中,这个事务都不允许回滚。由于时间戳总是增长,并且回滚后再重启的事务不被赋予新的时间戳,被回滚的事务最终变成最小时间戳事务,从而不会再次回滚
-
在wait-die机制中,较老的事务必须等待较新的事务释放它所持有的数据项。因此,事务越老,越要等待。在wound-die机制中,老的事务从不等待
-
在wait-die机制中,如果事务Ti由于申请的数据项当前被Tj持有而死亡并回滚,则当事务Ti重启时,它可能重新发出相同的申请序列。如果该数据项仍被Tj持有,则Ti将再度死亡。所以,在wound-die机制中,回滚可能较少。
-
二者的共同问题是:发生不必要的回滚
结论
- 在操作系统中广为采用的预防死锁的策略并不很适合数据库的特点
- DBMS在解决死锁的问题上更普遍采用的是诊断并解除死锁的方法
死锁检测
- 允许死锁发生
- 解除死锁
- 由DBMS的并发控制子系统定期检测系统中是否存在死锁
一旦检测到死锁,就要设法解除
检测死锁:超时法
- 如果一个事务的等待时间超过了规定的时限,就认为发生了死锁
- 优点:实现简单
- 缺点:
- 有可能误判死锁
- 时限若设置得太长,死锁发生后不能及时发现
等待图法
- 用事务等待图动态反映所有事务的等待情况
- 事务等待图是一个有向图G=(V,E)
- V为顶点的集合,每个顶点表示正运行的事务
- E为边的集合,每条边表示事务等待的情况
- 若Ti等待Tj,则Ti,Tj之间划一条有向边,从Ti指向Tj
- 当事务Tj不再持有事务Ti所需要的数据项时,边从等待图中删除
- 并发控制子系统周期性地检测事务等待图,如果发现图中存在回路,则表示系统中出现了死锁。
从死锁中恢复
- 解除死锁
- 选择牺牲者(应该使事务回滚的代价最小)
- 事务已经计算了多久,在事务完成前该事务还需要计算多长时间
- 该事务已经使用了多少数据项;为了完成该事务,还需要使用多少数据项
- 回滚时将牵扯多少事务
- 回滚
- 全部回滚事务
-部分回滚事务:事务只回滚到可以解除死锁处 - 饿死
- 同一事务总是被选为牺牲者,可以通过在代价因素中增加回滚次数,减少饿死
多粒度封锁的必要性(疑惑)
- 事务访问数据的粒度不同
DB、Table、Tuple、…
- 单一封锁粒度的问题
- 封锁粒度大:并发性低
- 封锁粒度小:访问大粒度数据加锁量巨大
- 多粒度封锁
- 根据访问数据的粒度,确定封锁的粒度
- 目的是封锁量有限,并可获得最大的并发性
多粒度封锁的基本原则
多粒度封锁判定授予锁的困难
- 申请小粒度锁的判定
- 判定在申请数据上有没有不相容锁
- 判定在申请数据相关大粒度数据上,有没有不相容锁
- 粒度的层次有限,本判定不困难
- 申请大粒度锁的判定
- 判定在申请数据上有没有不相容锁
- 判定在申请数据相关小粒度数据上,有没有不相容锁;
例如:对表加锁,要判定每个元组上有没有不相容锁
小粒度的数据量可能巨大,本判定困难
多粒度封锁
- 意向锁(intention lock mode):如果一个节点加上了意向锁,则意味着要对该节点的较低层节点进行显示加锁
- 在一个节点显式加锁之前,该结点的全部祖先均加上了意向锁
- 事务判定是否能够成功地给一个结点加锁时,不必搜索整棵树
- 共享意向锁(IS)/排他意向锁(IX)/共享排他意向锁(SIX)
多粒度封锁协议
- 增加了并发行,减少了锁开销
- 适应范围:
只存取几个数据项的短事务
由整个文件或一组文件形成报表的长事务
基于时间戳的协议
- 另一种解决事务可串行化的次序的方法是事先选定事务的次序。
- 时间戳排序协议的目标:
- 令调度冲突等价于按照事务开始早晚次序排序的串行调度;
时间戳排序协议的基本思想: - 开始早的事务不能读开始晚的事务写的数据
- 开始早的事务不能写开始晚的事务已经读过或写过的数据
- 调度可恢复方法(下列之一):
- 所有的写操作都在事务末尾执行,在写操作正在执行时,任何事务都不允许访问已写好的任何数据项
- 对未提交数据项的读操作,被推迟到更新该数据项的事务提交之后
- 事务Ti读取了其他事务所写的数据,只有在其他事务提交之后,Ti才能提交
事务的时间戳
数据项的时间戳
时间戳排序协议
时间戳协议性质和特点
- 保证冲突可串行化
- 冲突可串行化的调度不一定能被时间戳排序协议调度成功
- 无死锁
- 存在饥饿/饿死现象
(事务可能被反复回滚、重启) - 不能保证可恢复性
(可以扩展协议以保证可恢复性,如跟踪提交依赖等)
时间戳排序协议 vs 两阶段封锁协议
- 各有优缺点
- 都有本协议下合法、另一协议下不合法的调度
- 两协议以及多版本协议相结合,为多数dbms采用
Thomas写规则
Thomas写规则通过删除事务发出的过时的write操作产生视图等价于串行调度
- 按照时间戳排序协议,如果事务Ti由于发出read或者write操作而被并发控制机制回滚,则系统赋予它新的时间戳并重新启动。
删除与插入
谓词读和幻象现象
幻象现象的解决方案之一:
- 谓词读事务应该阻止其他事务在关系上创建、删除和更新满足谓词读条件的数据
- 为了找到满足谓词读条件的所有元组,谓词读事务必须搜索整个关系或者搜索关系的某一个索引
- 仅仅封锁满足谓词读条件的元组是不够的,应该通过锁定信息检测冲突
- 将数据项与关系关联,以表示有关关系包含哪些元组的信息
- 扫描关系的事务将获取数据项中的共享锁
- 插入或删除元组的事务会获取数据项上的独占锁(注意:数据项上的锁与单个元组上的锁不冲突。)
上述协议为插入/删除提供了非常低的并发性。
基于有效性检查的协议
- 在大部分事务是只读事务的情况下,事务发生冲突的频率较低
- 并发控制机制带来代码执行的开销及可能的事务延迟,应该采用开销较小的机制
- 减少开销面临的困难是我们事先不知道哪些事务将陷入冲突中。为了获得这些知识,需要一种监控系统的机制
多版本机制
软件版本包含两种不同含义:
- 为满足不同用户的不同使用要求,如适用于不同运行环境或不同平台的系列产品。
- 软件产品投入使用以后,经过一段时间运行提出了变更的要求,需要做较大的修正或纠错,增强功能或提高性能。
软件版本具有唯一标识作用。
α内部测试版
β内部测试版
-
版本控制软件提供完备的版本管理功能,用于存储、追踪目录(文件夹)和文件的修改历史,是软件开发者的必备工具,是软件公司的基础设施。版本控制软件的最高目标,是支持软件公司的配置管理活动,追踪多个版本的开发和维护活动,及时发布软件。
-
Git是一个开源的分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理。
-
GitHub 本质上是一个代码托管平台,它提供的是基于 Git 的代码托管服务。对于一个团队来说,即使不使用 GitHub,他们也可以通过自己搭建和管理 Git 服务器来进行代码库的管理,甚至还有一些其它的代码托管商可供选择。
-
Git 是一种方法,而 GitHub 只是使用这种方法的一个代码仓库。方法只有一个,而采用这种方法的却又很多个,类似 Github 和码云等。
-
多版本并发机制:每个write(Q)操作创建数据项Q的一个新版本。当事务发出read(Q)操作时,并发控制管理控制器选择Q的一个版本进行读取
-
多版本时间戳排序协议的良好特性:读请求从不失败且不必等待。在数据库系统中,读操作比写操作频繁,因此,此特性对于实践来说至关重要。
-
缺点:读取数据项可能更新R-timestamp字段,增加了一次磁盘访问机会;事务间的冲突通过回滚解决而不是等待,这种做法开销大
-
版本删除(多版本时间戳排序协议):
-
假设某数据项的两个版本Qk与Qj,这两个版本的W-timestamp都小于当前系统中最老的事务的时间戳,则Qk和Qj中较旧的的版本将不再用到,可以删除
-
多版本两阶段封锁协议:将多版本并发控制机制的优点与两阶段封锁协议的优点结合起来。该协议对只读事务和更新事务加以区分。
-
更新事务执行强两阶段封锁协议
-
数据项的每一个版本有一个时间戳,时间戳由计数器(ts-counter)实现
-
只读事务执行读操作时遵从多版本时间戳排序协议
按照多版本两阶段封锁协议
- 当更新事务读取一个数据项时,首先获得该数据项上的排它锁,然后为该数据项创建一个新版本,写操作在新版本上进行,新版本的时间戳最初置为∞
- 更新事务Ti完成任务后,按照如下方式提交:首先,Ti将它创建的每一个版本的时间戳设为当前ts-counter的值加1;然后Ti将ts-counter的值加1
版本删除(多版本两阶段封锁协议):
假设某数据项的两个版本Qk与Qj,这两个版本的时间戳都小于或等于当前系统中最老的事务的时间戳,则Qk和Qj中较旧的的版本将不再用到,可以删除
END