数据库概论第十一章-并发控制
并发控制概述
事务是并发控制的基本单位
为了保证事务的隔离性和一致性, 数据库管理系统需要对并发操作进行正确调度。 这些就是数据库管理系统中并发控制机制的责任。
[例 11.1] 考虑飞机订票系统中的一个活动序列:
① 甲售票点( 事务乃) 读出某航班的机票余额 A, 设 A=16。
② 乙售票点( 事务 T2) 读出同一航班的机票余额 A, 也为 16。
③ 甲售票点卖出一张机票, 修改余额 A<–A-1, 所以 A 为 15, 把 A 写回数据库。
④ 乙售票点也卖出一张机票, 修改余额 A<–A-1, 所 以 A 为 15, 把 A 写回数据库。
结果明明卖出两张机票, 数据库中机票余额只减少 1。
这种情况称为数据库的不一致性。 这种不一致性是由并发操作引起的。 在并发操作情况下, 对 T,、 T2 两个事务的操作序列的调度是随机的。 若按上面的调度序列执行, T,事务的修改就被丢失。 这是由于第 4 步中丁2 事务修改 A 并写回后覆盖了乃事务的修改。
下面把事务读数据 x 记为 R(x), 写数据 x 记为 W(x)。
并发操作带来的数据不一致性包括丢失修改、 不可重复读和读“ 脏” 数据。
丢失修改( lostupdate)
两个事务T1和T2读入同一数据并修改, T2 提交的结果破坏了 T1提交的结果, 导致T1的修改被丢失, 如图 11.2(a)所示。 例 11.1 的飞机订票例子就属此类
不可重复读 ( non-repcatable read )
不可重复读是指事务T1读取数据后,事务T2执行更新操作,使T1无法再现前一次读取结果。具体地讲,不可重复读包括三种情况:
(1)事务T1读取某一数据后,事务T2对其进行了修改,当事务T1再次读该数据时,得到与前一次不同的值。例如在图11.2(b)中,T1读取B=100进行运算,T2读取同一数据B,对其进行修改后将 B=200 写回数据库。 T1为了对读取值校对重读 B, B 已为 200, 与第一次读取值不一致。
(2) 事务T1按一定条件从数据库中读取了某些数据记录后, 事务T2删除了其中部分记录, 当T1再次按相同条件读取数据时, 发现某些记录神秘地消失了。
(3) 事务 T1按一定条件从数据库中读取某些数据记录后, 事务 T2 插入了一些记录,当T1再次按相同条件读取数据时, 发现多了一些记录。
后两种不可重复读有时也称为幻影( phantom row) 现象。
读“ 脏” 数据 (dirty read)
读“脏”数据是指事务T1修改某一数据并将其写回磁盘,事务T2读取同一数据后,T1由于某种原因被撤销,这时被T1修改过的数据恢复原值,T2 读到的数据就与数据库中的数据不一致,则T2读到的数据就为“脏”数据,即不正确的数据。例如在图11.2(c)中T将C值修改为200, T2 读到C为200,而T由于某种原因撤销,其修改作废,C恢复原值100,这时T2读到的C为200,与数据库内容不一致,就是“脏”数据。
并发控制的主要技术有封锁( locking)、 时间戳 ( timestamp), 乐观控制法( optimisticscheduler) 和多版本并发控制( multi-version concurrency control , MVCC) 等。
封 锁
所谓封锁就是事务 T 在对某个数据对象例如表、 记录等操作之前, 先向系统发出请求, 对其加锁。 加锁后事务 T 就对该数据对象有了一定的控制, 在事务 T 释放它的锁之前, 其他事务不能更新此数据对象。
基本的封锁类型有两种: 排他锁(exclusive locks, 简称 X 锁) 和共享锁( share locks, 简称 S 锁)。
排他锁又称为写锁。 若事务 T 对数据对象 A 加上 X 锁, 则只允许 T 读取和修改 A,其他任何事务都不能再对 A 加任何类型的锁, 直到 T 释放 A 上的锁为止。 这就保证了其他事务在 T 释放 A 上的锁之前不能再读取和修改 A。
共享锁又称为读锁。 若事务 T 对数据对象 A 加上 S 锁, 则事务 T 可以读 A 但不能修改 A, 其他事务只能再对 A 加 S 锁, 而不能加 X 锁, 直到 T 释放 A 上的 S 锁为止。 这就保证了其他事务可以读 A, 但在 T 释放 A 上的 S 锁之前不能对 A 做任何修改。
简而言之,就是读锁会阻塞写,但是不会阻塞读。而写锁,则既会阻塞读,又会阻塞写
封 锁 协 议
何时申请 X 锁或 S 锁、 持锁时间、 何时释放等。 这些规则称为封锁协议 ( locking protocol ) 。对封锁方式制定不同的规则, 就形成了各种不同的封锁协议。 本节介绍三级封锁协议。
一级封锁协议
一级封锁协议是指, 事务 T 在修改数据 R 之前必须先对其加 X 锁, 直到事务结束才释放。
一级封锁协议可防止丢失修改, 并保证事务T是可恢复的。例如图11.4(a)使用一-封锁协议解决了图11.2(a)中的丢失修改问题。
在一级封锁协议中, 如果仅仅是读数据而不对其进行修改, 是不需要加锁的, 所以它不能保证可重复读和不读“ 脏” 数据
图11.4(a)事务T1在读A进行修改之前先对A加X锁,当T2再请求对A加X锁时被拒绝,T2只能等待T1:释放A上的锁后获得对A的X锁,这时它读到的A已经是T1,更新过的值15,再按此新的A值进行运算,并将结果值A=14写回到磁盘。这样就避免了丢失T1的修改。
二级封锁协议
二级封锁协议是指, 在一级封锁协议基础上增加事务 T 在读取数据 R 之前必须先对其加 S 锁, 读完后即可释放 S 锁。
级封锁协议除防止了丢失修改, 还可进一步防止读“ 脏” 数据。 例如图 11.4(c)使用二级封锁协议解决了图 11.2(c)中的读“ 脏” 数据问题。
图11.4(c)中,事务T1在对C进行修改之前,先对C加X锁,修改其值后写回磁盘。这时T2请求在C上加S锁,因T,已在C上加了X锁,T2只能等待。T因某种原因被撤销,C恢复为原值100, TI释放C上的X锁后T2获得C上的S锁,读C=100。这就避免了T2读“脏”数据。
三 级 封 锁 协 议
三级封锁协议是指, 在一级封锁协议的基础上增加事务 T 在读取数据 R 之前必须先对其加 S 锁, 直到事务结束才释放。
三级封锁协议除了防止丢失修改和读“ 脏” 数据外, 还进一步防止了不可重复读。
图11.4(b)中,事务T1在读A、B之前,先对A、B加S锁,这样其他事务只能再对A、B加S锁,而不能加X锁,即其他事务只能读A、B,而不能修改它们。所以当T2为修改B而申请对B的X锁时被拒绝,只能等待T1释放B上的锁。T1为验算再读A、B,这时读出的 B 仍是 100, 求和结果仍为 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有可能永远等待,这就是活锁的情形,如图11.5(a)所示。
避免活锁的简单方法是采用先来先服务的策略。
死锁
如果事务T1封锁了数据R1, T2封锁了数据R2,然后T1请求封锁R2,因T2已封锁了R2, 于是T1等待T2释放R2上的锁:接着T2又申请封锁R1, 因T1已封锁了R1, T2也只能等待T1释放R1上的锁。这样就出现了T1在等待T2,而T2又在等待T1的局面,T1和T2两个事务永远不能结束,形成死锁。如图11.5(b)所示。
目前在数据库中解决死锁问题主要有两类方法, 一类方法是釆取一定措施来预防死锁的发生, 另一类方法是允许发生死锁, 采用一定手段定期诊断系统中有无死锁, 若有则解除之。
死锁的预防
(1) 一次封锁法
一次封锁法要求每个事务必须一次将所有要使用的数据全部加锁,否则就不能继续执行。图11.5(b)的例子中,如果事务T1将数据对象R1和R2一次加锁,T1 就可以执行下去,而T2等待。T1执行完后释放R1、R2上的锁,T2继续执行。这样就不会发生死锁。
一次封锁法虽然可以有效地防止死锁的发生, 但也存在问题, 第一, 一次就将以后要用到的全部数据加锁, 势必扩大了封锁的范围, 从而降低了系统的并发度; 第二, 数据库中数据是不断变化的, 原来不要求封锁的数据在执行过程中可能会变成封锁对象, 所以很难事先精确地确定每个事务所要封锁的数据对象, 为此只能扩大封锁范围, 将事务在执行过程中可能要封锁的数据对象全部加锁, 这就进一步降低了并发度
(2) 顺序封锁法
顺序封锁法是预先对数据对象规定一个封锁顺序, 所有事务都按这个顺序实施封锁。例如在 B 树结构的索引中, 可规定封锁的顺序必须是从根结点开始, .然后是下一级的子结点, 逐级封锁。
第一, 数据库系统中封锁的数据对象极多, 并且随数据的插入、 删除等操作而不断地变化, 要维护这样的资源的封锁顺序非常困难, 成本很高; 第二, 事务的封锁请求可以随着事务的执行而动态地决定, 很难事先确定每一个事务要封锁哪些对象, 因此也就很难按规定的顺序去施加封锁。
死锁的诊断与解除
(1) 超时法
如果一个事务的等待时间超过了规定的时限, 就认为发生了死锁。 超时法实现简单,但其不足也很明显, 一是有可能误判死锁, 如事务因为其他原因而使等待时间超过时限,系统会误认为发生了死锁; 二是时限若设置得太长, 死锁发生后不能及时发现。
(2) 等待图法
事务等待图动态地反映了所有事务的等待情况。 并发控制子系统周期性地( 比如每隔数秒) 生成事务等待图, 并进行检测。 如果发现图中存在回路, 则表示系统中出现了死锁。
并发调度的可串行性
可串行化调度
定义 多个事务的并发执行是正确的, 当且仅当其结果与按某一次序串行地执行这些事务时的结果相同, 称这种调度策略为可串行化( serializable) 调度。
可串行性( serializability) 是并发事务正确调度的准则。 按这个准则规定, 一个给定的并发调度, 当且仅当它是可串行化的, 才认为是正确调度。
冲突可串行化调度
冲突操作是指不同的事务对同一个数据的读写操作和写写操作
若一个调度是冲突可串行化, 则一定是可串行化的调度。 因此可以用这种方法来判断一个调度是否是冲突可串行化的。
应该指出的是, 冲突可串行化调度是可串行化调度的充分条件, 不是必要条件。 还有不满足冲突可串行化条件的可串行化调度。
两段锁协议
目前数据库管理系统普遍釆用两段锁( TwoPhase Locking, 简称2PL) 协议的方法实现并发调度的可串行性, 从而保证调度的正确性。
所谓两段锁协议是指所有事务必须分两个阶段对数据项加锁和解锁:
- 在对任何数据进行读、 写操作之前, 首先要申请并获得对该数据的封锁;
- 在释放一个封锁之后, 事务不再申请和获得任何其她封锁
所谓“两段” 锁的含义是, 事务分为两个阶段, 第一阶段是获得封锁, 也称为扩展阶段, 在这个阶段, 事务可以申请获得任何数据项上的任何类型的锁, 但是不能释放任何锁; 第二阶段是释放封锁, 也称为收缩阶段, 在这个阶段, 事务可以释放任何数据项上的任何类型的锁, 但是不能再申请任何锁
事务遵守两段锁协议是可串行化调度的充分条件 , 而不是必要条件。
也就是说,若并发事务都遵守两段锁协议,那么对这些事务的任何并发调度策略都是可串行化的; 但是, 若并发事务的一个调度是可串化的, 不一定所有事务都符合两段锁协议。 例如图 11.7(d)是可串行化调度, 但T1和T2不遵守两段锁协议
封锁的粒度
封锁对象的大小称为封锁粒度( granularity )。 封锁对象可以是逻辑单元, 也可以是物理单元。 以关系数据库为例, 封锁对象可以是这样一些逻辑单元: 属性值、 属性值的集合、元组、 关系、 索引项、 整个索引直至整个数据库; 也可以是这样一些物理单元: 页( 数据页或索引页)、 物理记录等
封锁粒度与系统的并发度和并发控制的开销密切相关。 直观地看, 封锁的粒度越大,数据库所能够封锁的数据单元就越少, 并发度就越小, 系统开销也越小; 反之, 封锁的粒度越小, 并发度较高, 但系统开销也就越大。
多粒度封锁
多粒度树的根结点是整个数据库, 表示最大的数据粒度。 叶结点表示最小的数据粒度。
多粒度封锁协议允许多粒度树中的每个结点被独立地加锁。 对一个结点加锁意味着这个结点的所有后裔结点也被加以同样类型的锁。 因此, 在多粒度封锁中一个数据对象可能以两种方式封锁, 显式封锁和隐式封锁。
显式封锁是应事务的要求直接加到数据对象上的锁; 隐式封锁是该数据对象没有被独立加锁, 是由于其上级结点加锁而使该数据对象加上了锁。
多粒度封锁方法中, 显式封锁和隐式封锁的效果是一样的, 因此系统检查封锁冲突时不仅要检査显式封锁还要检査隐式封锁。
这样的检査方法效率很低。 为此人们引进了一种新型锁, 称为意向锁( intention lock)。 有了意向锁, 数据库管理系统就无须逐个检査下一级结点的显式封锁。
意向锁
意向锁的含义是如果对一个结点加意向锁, 则说明该结点的下层结点正在被加锁; 对任一结点加锁时, 必须先对它的上层结点加意向锁。
下面介绍三种常用的意向锁: 意向共享锁( Intent Share Lock,IS 锁); 意向排他锁( IntentExclusive Lopk, IX 锁); 共享意向排他锁 ( Share Intent Exclusive Lock, SEX 锁)。
1. IS 锁
如果对一个数据对象加 IS 锁, 表示它的后裔结点拟( 意向) 加 S 锁。例如, 事务 T1对R1中某个元组加 S 锁, 则要首先对关系R1和数据库加 IS 锁。
2. IX 锁
如果对一个数据对象加 IX 锁, 表示它的后裔结点拟( 意向) 加 X 锁。 例如, 事务T1要对R1中某个元组加 X 锁, 则要首先对关系R1和数据库加 IX 锁。
3. SIX 锁
如果对一个数据对象加 SIX 锁, 表示对它加 S 锁, 再加 IX 锁, 即 SIX=S+IX。 例如对某个表加 SIX 锁, 则表示该事务要读整个表( 所以要对该表加 S 锁), 同时会更新个别元组( 所以要对该表加 IX 锁)。