并发控制
最常用的机制有两阶段封锁和快照隔离。
基于锁的协议
确保隔离性的方法之一是要求对数据项以互斥方式进行访问。 实现该需求最常用的方法是只允许事务访问当前该事务持有锁(lock)的数据项。
锁
- 共享的:
如果事务T获得了数据项Q上的共享型锁(记为S),则 T可读但不能写Q。 - 排他的:
如果事务T获得了数据项Q上的排他型锁(记为X),则 T既可读又可写Q。
要求每个事务都要根据自己将对数据项Q进行的操作类型申请适当的锁。 事务只有在并发控制管理器授予所需锁后才能继续其操作。 这两种锁类型的使用可以让多个事务读取一个数据项但是限制同时只能有一个事务进行写操作。
对于给定的一个锁类型集合,可在它们上按如下方式定义一个相容函数:令A与B代表任意的锁类型,假设事务
T
i
T_i
Ti?请求对数据项Q加A类型锁,而事务
T
j
(
T
i
≠
T
j
)
T_j(T_i \neq T_j)
Tj?(Ti??=Tj?)当前在数据项Q上拥有B类型锁。 尽管数据项Q上存在B类型锁,如果事务
T
i
T_i
Ti?可以立即获得数据项Q上的锁,则我们就说A类型锁与B类型锁是相容的。
一个事务只要还在访问数据项,它就必须拥有该数据项上的锁。此外,让事务在对数据项作最后一次访问后立即释放该数据项上的锁也未必是可取的,因为有可能不能保证可串行化。 两个事务并发的执行,可能出现如下调度1。 该调度显示了事务执行的动作以及并发控制管理授权加锁的时刻。 申请加锁的事务砸并发控制管理器授权加锁之前不能执行下一个动作。
假定事务结束后才释放锁。 封锁可能导致一种不受欢迎的情形。 进入一种哪个事务都不能正常执行的状态,这种情形称为死锁。 死锁发生时,系统必须回滚两个事务中的一个。
通过添加锁的机制,我们保证在执行完对数据项加锁步骤后,即使后续转而调度其他事务某步骤,也让其因为阻塞,而放弃调度机会。 进而避免了共享数据的一些冲突修改使数据不一致。 但加锁后,若释放过早,可能仍然无法保证数据一致性。
加锁带来的一个副作用是,不合理的加锁顺序,某些情况下会产生死锁。 如事务A持有数据项X的锁,申请Y的锁,而事务B持有数据项Y的锁,申请X的锁。此时产生死锁,两者都无法继续执行。
封锁协议是一组规则: 规定事务何时对数据项们进行加锁、解锁。 封锁协议限制了可能的调度数目。这些调度组成的集合是所有可能跟的可串行化调度的一个真子集。
调度S满足封锁协议A时,称S在A下是合法的调度。 若满足协议A的调度可保证为冲突可串行化调度,则封锁协议可保证冲突可串行性。
锁的授予
饿死: 指的是当前事务A申请对数据X加写锁,X当前已被事务B加了共享读锁,A等待中持续有一系列对X共享读请求,均被满足,故而A持续等待的现象(事务A可能永远不能取得进展)。
可通过按如下方式授权加锁来避免事务饿死:当事务A申请对数据项X加M型锁时,并发管理器授权加锁的条件是:
- 1.不存在在数据项X上持有与M型锁冲突的锁的其他事务。
- 2.不存在等待对数据项X加锁且先于A申请加锁的事务。
因此,一个加锁请求就不会被其后的加锁申请阻塞。
两阶段封锁协议
保证可串行性的一个协议是两阶段封锁协议。
该协议要求每个事务分两个阶段提出加锁和解锁申请。:
- 增长阶段
可获得锁,不可释放锁。 - 缩减阶段
可释放锁,不可获得锁。
对任何事务调度中该事务获得其最后加锁的位置称为事务的封锁点。 按封锁点顺序,得到一个串行化调度是满足封锁协议调度对应的冲突可串行化等价调度。
要进一步保证无级联性质(避免级联回滚),需严格两阶段封锁协议,既要求两阶段,又要求事务持有的排他锁必须在事提交后方可释放。
另一类似变体:强两阶段封锁协议,既要求两阶段,又要求事务提交前不得释放任何锁。
基本两阶段+允许锁转换 升级:将持有的共享锁升级为排他锁,只能发生在增长阶段。 升级时,若有其他事务持有此数据项的共享锁,需阻塞等待。
降级:将持有的排他锁降级为共享锁,只能发生在缩减阶段。
这样的协议,也可保证产生的调度是可串行化的(产生的调度,指整个实际执行流程,中间调度到时因为阻塞,而未执行的,不出现在执行流程中.阻塞步骤出现在轮到其执行且条件满足,可执行时刻的位置上)
排他锁直到事务结束才释放下,可保证无级联。
封锁的实现
锁管理器可以实现为一个过程,它从事务接受消息并反馈消息。 锁管理器过程针对锁请求消息返回授予锁消息,或者要求事务回滚的消息(发生死锁时)。 解锁消息只需要得到一个确认回答,但可能引发为其他等待事务的授予锁消息。
锁管理器使用以下数据结构: 为目前已经加锁的每个数据项维护一个链表,每一个请求为链表中一条记录,按请求到达的顺序排序。
它使用一个数据项名称为索引的散列表来查找链表中的数据项(如果有的话);这个表叫做锁表。
一个数据项的链表中每一条记录表示由哪个事务提出的请求,以及它请求什么类型的锁,该记录还表示该请求是否已授予锁。
锁表还应当维护一个基于事务标识符的索引,这样它可以有效地确定给定事务持有的锁集。
锁管理器这样处理请求:
- 一个对某数据项加锁请求发出时,先定位数据项。
若数据项不存在,添加.并组织关联到该数据项的链表,授予锁。 若数据项存在,检查申请锁和当前事务项上锁是否冲突。 若不冲突,进一步判断是否有先到的针对此数据项的加锁申请,若有,等待先前请求满足下,才授予。若无,先到等待状态的加锁申请,授予,更新链表。 若冲突,请求等待,更新链表。 - 一个对某数据项的解锁请求发出时,先定位数据项,删除其关联链表中相关元素,检查是否存在排队等待请求。
若存在,检查队首请求是否可被授权,若可以,则授权。否则,不授权。 完成对队首元素授权后,可继续检查新的队首元素。 - 事务中止时,
此事务关联的尚未满足的加锁申请将被取消。 事务撤消时,事务已经持有的锁将被释放。
基于图的协议
前提:需要知道所有需要访问的数据项及其访问顺序。
依据参与并发事务集合产生的数据项集,数据项顺序要求构造一个图,可以视为有向无环图,称为数据库图。
在树形协议中,可用的加锁指令只有 lock-X 。
单个事务对一个数据项最多能加一次锁,并且遵从以下规则:
- 首次加锁,可直接对数据项加锁。
- 非首次,加锁数据项前,需依次先对数据项各个祖先加锁。
- 解锁,随时可以。
- 数据项在同一事务中只可被加锁一次(即数据项被A加锁并解锁后,A不能再对该数据项加锁)。
可保证产生的符合协议的合法调度,是冲突可串行化的。
优点:不会产生死锁。 缺点:非首次,对数据项加锁前,需依次先对数据项各个祖先加锁。
死锁处理
存在一个事务环路,环路中每个事务因为下一个事务持有的资源而阻塞(每个事务本身持有造成上一事务阻塞的资源)。
死锁产生时,解决是选择一个或若干事务,让其回滚到获得某个锁时刻之前,它在该点得到一个锁,而释放该锁就可以解决死锁。
处理死锁问题的两类方法:
- 死锁预防协议
保证系统永不进入死锁状态。 - 死锁检测和死锁恢复机制
允许进入死锁,通过死锁检测检测死锁,通过死锁恢复进行恢复。
死锁预防
死锁预防有两种方法:
-
对加锁请求进行排序或要求同时获得所有锁来保证不会发生循环等待。 最简单的机制是:事务开始前一次封锁所有涉及数据项。此外,要么一次全部封锁要么全不封锁。 缺点: 1)在事务开始前通常很难预知哪些数据项需要封锁。 2)数据项使用率可能很低,因为许多数据项可能封锁很长时间却用不到。 另一种机制:对数据项指定顺序,同时要求事务需按规定顺序加锁数据项。 该方法的一个变种是使用数据项与两阶段封锁关联的全序。 -
每当有可能死锁时,进行事务回滚。 使用抢占和事务回滚。 事务B持有A的锁,可能将B回滚让其释放锁,来满足A。 给每个事务赋一个唯一的时间戳,系统用时间戳来决定事务应等待还是回滚。 利用时间戳的两种不同的死锁预防机制: 1.wait-die——非抢占。 事物A申请的数据项被事务B持有。 若A的时间戳小于B,则A等待;若A的时间戳大于B,则A回滚。 2.wound-wait——抢占(时间戳越小优先级越高)。 事务A申请的数据项被事务B持有。 若A的时间戳大于B的时间戳,则A等待;若A的时间戳小于B的时间戳,B回滚。
另一种处理死锁的简单方法基于锁超时。 超时时,事务回滚。
超时时间合理确定是一个问题。一般而言很难确定一个事务超时之前应等待多长时间。如已发生死锁,等待时间太长导致不必要的延迟。如果等待时间太短,即便没有死锁,也可能引起事务回滚,造成资源浪费。 该机制也可能会产生饿死。
死锁检测与恢复
如果系统没有采用能保证不产生死锁的协议,那么系统必须采用检测与恢复机制。
检查系统状态的算法周期性地激活,判断有无死锁发生。如果发生死锁,则系统必须试着从死锁中恢复。
系统必须:
- 维护当前将数据项分配给事务的有关信息,尚未解决数据项的请求信息。
- 提供一个使用这些信息判断系统是否进入死锁状态的算法。
- 当检测算法检测存在死锁时,从死锁恢复。
死锁检测
死锁可以用称为等待图的有向图来精确描述。 该图由 G=(V,E)对组成,其中V是顶点集,E是边集。
每个顶点代表一个事务,若事务A在等待事务B释放某数据项,则存在边A–>B。
当且仅当等待图包含环时,系统中存在死锁。 在该环中的每个事务称为处于死锁状态。
上图所示的等待图,说明了下面这些情形:
- 事务T17在等待事务T18与T19。
- 事务T19在等待事务T18。
- 事务T18在等待事务T20。
由于该等待图无环,因此系统没有处于死锁状态。
死锁频繁发生时,检测激活频率也应相应增加。
从死锁中恢复
当一个检测算法判定存在死锁时,系统必须从死锁中恢复。
解除死锁最通常的做法是回滚一个或多个事务。 需采取的动作有三个:
- 1.选择牺牲者
衡量因素: 1.事务已经计算了多久,离完成将将计算多久。 2.事务已经使用了多少数据项,离完成还需使用多少数据项。 3.回滚时将牵涉多少事务。 - 2.回滚
彻底回滚:中止该事务,然后重新开始。 部分回滚:要求系统维护所有正在运行事务的额外状态信息。 死锁检测机制应确定为打破死锁,选定的事务要释放哪些锁。 - 3.饿死
同一事务总是被选为牺牲者时的现象。 可在选择时考虑到事务的已被回滚次数。
多粒度
对一个数据项逻辑集合加锁时,传统是对集合内每个数据项依次加锁。 多级粒度下,允许对代表逻辑集合的一个结点进行一次加锁。
一个特点: 从根到每个节点有且只有一条路径。 加锁类型:共享锁,排他锁。 对某节点A加锁B时,隐式地对节点A所有后代节点加锁B。
事务请求对某个节点加锁T时,从树根到节点路径上每个节点X,依次判定节点X目前持有锁的类型,与请求的锁T是否相容; 若发现不相容,请求阻塞等待。
意向锁类型 在给节点A显式加锁T的过程中,一方面,非节点A加了显式的锁T;一方面,非A的所有后台节点加了隐式的锁T。 另一方面,对从根到A的路径上每个A的祖先节点给其加了意向锁T。
与共享锁相关联的是一种意向锁,与排他锁相关联的是另一种意向锁。 如果一个结点加上了共享型意向锁(IS),那么将在树的较低层进行显示封锁,但只能加共享锁。 如果一个结点加排他型意向锁(IX),那么将在树的较低层简写显示封锁,可以加排他锁或共享锁。 最后,若一个结点加上了共享排他型意向锁(SIX),则以该结点为根的子树显示地加了共享锁,并且将在树的更低层显示地加排他锁。
多粒度封锁协议保证可串行化规则:
- 仅当对Q的父亲节点有IX,IS锁时,对Q可加S或IS。
- 对Q的父亲节点有IX锁时,对Q可加X,IX锁。
- 事务是两个阶段,阶段一只加锁,阶段二只解锁。
- 对Q的子结点均无锁时,可对Q解锁。
该协议增强了并发性,减少锁开销。它在由如下事务类型混合而成的应用中尤其有用:
- 只访问几个数据项的短事务。
- 由整个文件或一组文件来生成报表的长事务。
基于时间戳的协议
时间戳
对于系统中每个事务
T
i
T_i
Ti?,把一个唯一的固定时间戳和它联系起来,此事件戳记为
T
S
(
T
i
)
TS(T_i)
TS(Ti?)。 该时间戳是在事务
T
i
T_i
Ti?开始执行前由数据库系统赋予的。 若事务
T
i
T_i
Ti?已赋予时间戳
T
S
(
T
i
)
TS(T_i)
TS(Ti?),此时有一新事务
T
j
T_j
Tj?进入系统,则
T
S
(
T
i
)
<
T
S
(
T
j
)
TS(T_i) < TS(T_j)
TS(Ti?)<TS(Tj?)。
实现这种机制可以采用下面这两个简单的方法:
- 1.使用系统时钟的值作为时间戳;即,事务的时间戳等于该事务进入系统时的时钟值。
- 2.使用逻辑计数器,每赋予一个时间戳,计数器增加计数;即,事务的时间戳等于该事务进入系统时的计数器值。
系统保证所产生的调度等价于按时间戳排序后的串行调度(时间戳小的在前,大的在后)。
每个数据项关联两个时间戳:
- **W-timestamp(Q):**成功执行write(Q)的所有事务的最大时间戳。
- **R-timestamp(Q):**成功执行read(Q)的所有事务的最大时间戳。
时间戳排序协议
时间戳排序协议保证任何有冲突的 read 或 write 操作按时间戳顺序执行。
该协议运作方式如下:
-
假设事务A发出read(Q) a. 若TS(A) < W-timestamp(Q) 表明,A之后的某事务已经写了Q。 A回滚,重新开始并获得新的时间戳。 b. 若TS(A)>=W-timestamp(Q) 表明,A之前的某事务写了Q。 按串行化假设,此事务在A前终止。 A此时读Q是可以的。 更新Q的读时间戳为所有读它事务时间戳中最大的。 -
假设事务A发出write(Q) a. 若TS(A)<R-timestamp(Q) A之后的某事务读了Q的数据。 此时若允许A去写,则无法完成串行化的要求。 A回滚,以新的时间戳重新开始。 b. 若TS(A)<W-timestamp(Q) A之后的某事务K写了Q。 此时若允许A去写,造成一个事实:K先写Q,A后写Q. 这样无法完成A-Q的冲突可串行化--将A的所有操作提到Q的前面。 A回滚,以新的时间戳重新开始。 c. 其他情况 执行write,将W-timestamp(Q)更新为TS(A)。
按上述协议可保证冲突可串行化。
该协议可能产生不可恢复的调度。然而,该协议可以进行扩展,用以下几种方法之一来保证调度可恢复:
- 在事务末尾执行所有的写操作能保证可恢复性和无级联性,这些写操作必须具有下述意义的原子性:在写操作正在执行的过程中,任何事务都不许访问已写完的任何数据项。
- 可恢复性和无级联性也可以通过使用一个受限的封锁形式类保证,由此,对未提交数据项的读操作被推迟到更新数据项的事务提交之后。
- 可恢复可以通过跟踪未提交写操作来单独保证,一个事务T读取了其他事务所写的数据,只有在其他事务都提交之后,T才能提交。
Thomas 写规则
通过对写操作第2中情况由回滚改为忽略,得到的规则称为Thomas规则。
通过忽略写,Thomas写规则允许非冲突可串行化但是正确的调度。 这些允许的非冲突可串行化调度满足视图可串行化调度的概念。
Thomas写规则实际上是通过删除事务发出的过时的write操作来使用视图可串行化。 对事务的这种修改使得系统可以产生其他协议所不能产生的可串行化调度。
考虑两个调度S,S’.参与调度的事务集一致。 满足以下三个条件,称为S,S’视图等价。 1.对每个数据项Q,若S中事务A读取了Q的初始值,则S’中事务A也需读取Q的初始值。 2.对每个数据项Q,若S中事务A执行了read(Q),并且读取的值由事务B的某个write(B)产生。 在S’中事务A执行了read(Q).也需保证读取的值由事务B的同一个write(B)产生。 3.对每个数据项Q,若S中事务A执行了最后的write(Q),则S’中也需由事务A执行最后的write(Q)。 如果调度A视图等价于一个串行调度S,称此调度A对调度S是视图可串行化的。
基于有效性检查的协议
有效性检查协议要求每个事务
T
i
T_i
Ti?在其生命周期中按两个或三个阶段执行,这取决于该事务是一个只读事务还是一个更新事务。
这些阶段顺序地列在下面。
- 读阶段
在这一阶段中,系统执行事务
T
i
T_i
Ti?。 各数据项的值被读入并保存在事务
T
i
T_i
Ti?的局部变量中。 所有write操作,对局部临时变量进行的,并不对数据库进行真正的更新。 - 有效性检查阶段
对事务
T
i
T_i
Ti?进行有效性测试。判定是否可以执行write操作而不违反可串行性。如果事务有效性测试失败,则系统终止这个事务。 - 写阶段
若事务
T
i
T_i
Ti?已通过有效性检查,则保存
T
i
T_i
Ti?任何写操作结果的临时局部变量值被复制到数据库中。只读事务忽略这个阶段。
三个时间戳:
- 1.Start(
T
i
T_i
Ti?)
事务
T
i
T_i
Ti?开始执行的时间. - 2.Validation(
T
i
T_i
Ti?)
事务
T
i
T_i
Ti?完成读阶段,并开始有效性检查的时间(首个write前检查时间) - 3.Finish(
T
i
T_i
Ti?)
事务
T
i
T_i
Ti?完成写阶段时间。(最后一个write操作后)
事务
T
i
T_i
Ti?的有效性测试要求任何满足TS(
T
k
T_k
Tk?)<TS(
T
i
T_i
Ti?)的事务
T
k
T_k
Tk?必须满足下面两个条件之一: 1.Finish(
T
k
T_k
Tk?)<Start(
T
i
T_i
Ti?) 意味着
T
k
T_k
Tk?的所有写已经结束且更新到数据库后,
T
i
T_i
Ti?才开始。 安全,满足可串行化。 2.Start(
T
i
T_i
Ti?)<Finish(
T
k
T_k
Tk?)<Validation(
T
i
T_i
Ti?),意味着
T
k
T_k
Tk?开始写到
T
k
T_k
Tk?提交过程中或在
T
i
T_i
Ti?写之前,
T
i
T_i
Ti?读了某些数据项。
T
k
T_k
Tk?所写的数据项集与
T
i
T_i
Ti?所读数据项集不相交.意味着
T
k
T_k
Tk?的写不影响
T
i
T_i
Ti?的读,也不会影响
T
i
T_i
Ti?之后的写。 安全,满足可串行化。
在有效性检查机制中,由于事务乐观地执行,假定它们能够完成执行并且最终有效,因此也称为乐观的并发控制机制。 封锁和时间戳排序是悲观的,因为当它们检测到一个冲突时,它们强迫事务等待或回滚,即使该调度有可能是冲突可串行化的。
多版本机制
在多版本并发控制机制中,每个write(Q)创建Q的一个新版本。 当事务发出一个read(Q)时,并发控制管理器选择Q的一个版本读取。 并发控制必须保证用于读取的版本的选择能保持可串行性。
多版本时间戳排序
对系统中的每个事务A,将一个唯一的静态时间戳与之关联,记为TS(A)。 对每个数据项Q,有一个版本序列
<
Q
1
,
Q
2
,
.
.
.
,
Q
m
>
<Q_{1}, Q_{2}, ..., Q_{m}>
<Q1?,Q2?,...,Qm?> 与之关联,每个版本
Q
k
Q_{k}
Qk?含3个数据字段:
- Content
是
Q
k
Q_{k}
Qk?版本的值。 - W-timestamp(Q)
创建
Q
k
Q_{k}
Qk?版本的事务的时间戳。 - R-timestamp(Q)
所有成功读取
Q
k
Q_{k}
Qk?版本的事务的时间戳中的最大值。
此版本被创建时,值为创建该版本事务的时间戳。
多版本时间戳排序机制: 设事务A操作Q,选出
Q
k
Q_{k}
Qk?,其为在此事务前最后被写的那个版本。
- 1.如果事务A发出read,返回
Q
k
Q_{k}
Qk?内容,分析是否需更新
Q
k
Q_{k}
Qk?的读时间戳。
- 2.如果事务A发出write,若A的时间戳<最后读
Q
k
Q_{k}
Qk?事务的时间戳,回滚A;
若A的时间戳=W-timestamp(
Q
k
Q_{k}
Qk?),意味者
Q
k
Q_{k}
Qk?由事务A创建,且这里又再次去写(且此数据项尚未被A之前事务使用),覆盖
Q
k
Q_{k}
Qk?内容。 若A的时间戳>最后一次读
Q
k
Q_{k}
Qk?事务的时间戳,意味着A的写将创建一个尚未被任何其他事务使用的新版本。
不再需要的版本根据以下规则删除:假设有某数据项的两个版本
Q
k
Q_k
Qk?与
Q
j
Q_j
Qj?,这两个版本的 W-timestamp 都小于系统中最老的事务的时间戳,那么
Q
k
Q_k
Qk?和
Q
j
Q_j
Qj?中较旧的版本将不会再用到,因而可用删除。
多版本时间戳排序机制有一个很好的特性:读请求从不失败且不必等待。 也存在两个不好的特性:首先,读取数据项要求更新 R-timestamp 字段,于是产生两次潜在的磁盘访问而不是一次。其次,事务间的冲突通过回滚而不是等待来解决。这种做法开销可能很大。
多版本两阶段封锁
事务区分为只读事务,更新事务。
- 更新事务执行强两阶段封锁协议(持有锁,提交后,统一释放。)符号按提交次序的串行化。
- 只读事务
开始执行前,事务以当前计数器作为其时间戳。 只读事务的读遵从多版本时间戳排序协议。 - 更新事务
读一个数据项时,获得数据项上共享锁后,读其最新版本值。 写一个数据项时,获得数据项上排他锁,为此数据项创建新版本,在新版本上写。 - 更新事务的提交
将其创建的数据项的时间戳设为计数器值+1 更新计数器让其增1。 提交需保证原子性。
快照隔离
事务开始执行时给予其一份数据库的快照。 事务在快照上操作,和其他并发事务完全隔离开。
更新数据库的事务在提交时,需处理可能存在的冲突。 提交经过检查无冲突时,提交写入实际数据库需作为原子方式执行。
更新事务的有效性检验步骤
两个并发的事务可能更新同一数据项。
更新丢失: A写了X,B也写了X,A提交,B提交,此时X为B中版本,A中版本被覆盖,称为更新丢失。
给定事务A, 事务A的并发事务B:A开始到执行冲突检测之间若B活跃的或者部分提交的。
-
先提交者获胜 A进入部分提交状态,以下作为一个原子操作: 1.检查与T并发的每个事务S,对T打算写入的任一数据X,S是否已经将X写入。 2.如存在S,T终止。 3.如不存在S,T执行提交,原子方式写入所有更新到数据库。 -
先更新者获胜 采用一种仅用于更新操作的锁机制。 事务A试图更新一个数据项X,请求对X的写锁。 如果X可被授予,获得X的写锁后执行: 1.如果X已被任何并发事务更新,A终止。 2.如果X未被任何并发事务更新,执行操作(含提交到数据库)。 如果X不可被授予,阻塞等待。 若因为持有锁的事务提交释放锁而获得,终止。 若因为持有锁的事务被终止或释放锁定,进而这里得以获得。 进一步判定,X是否被已被其他事务所写。 若未被写,获得X的锁定,执行操作。
串行化问题
快照隔离不能保证可串行化。
设有两个并发事务A和B,以及两个数据项S和T。 假设A读S和T,然后更新T, 假设B读S和T,然后更新S。 这样产生的优先图,是有环的。
一对事务中每一个都读对方写的数据,但是不存在两者同时写的数据,这种情况称为写偏斜。
由数据库强制执行的完整性约束(如主键,外键约束),不可在快照上进行检查。
考虑两个并发更新事务本身可串行化,且不存在问题,但是当一个只读事务出现在某个时刻时正好会造成问题。 假设并发事务A和B,以及两个数据项S,T。 A读T,然后更新T,而B读S,T,然后更新S。 同时并发执行这两个事务没有问题。
假设A提交时B仍活跃。 假设A提交后B尚未提交时,新的只读事务C进入系统 C读取S和T。 C的快照包括A的更新,B的更新因为未提交故不再C的快照中 (串行化下C要先于B提交)。
某些非可串行化执行也可以得到一致性结果,但串行化执行结果一定符号一致性要求。
插入操作、删除操作与谓词读
- delete (Q):从数据库中删除数据项Q。
- insert (Q):插入一个新的数据项Q到数据库中并赋予Q一个初值。
删除
- 两阶段封锁协议下,删除数据项,需先拥有排他锁。
- 在时间戳排序协议下,必须执行类似于为 write操作进行的测试。
假设A执行delete(Q): 1.如果TS(A)<R-timestamp(Q),则Q此时被A之后某事务读取,不可delete,A回滚。 2.如果TS(A)<W-timestamp(Q),则Q此时被A之后某事务所写,不可delete,A回滚。 3.否则,执行delete操作。
插入
- 在两阶段封锁协议下,
如果A执行insert(Q),需持有Q的排他锁。 - 在时间戳排序协议下,
如果A执行insert(Q),就把R-timestamp(Q), W-timestamp(Q)设为TS(A)。
谓词读和幻象现象
一个事务A执行符合某条件A的元组访问。 一个事务B执行涉及某条件A的元组更新。 A与B串行顺序不同时,事务A访问的元组也不同,称这种现象为幻象现象。
仅封锁所访问的元组不够;用于找出元组的表的信息也需封锁。
索引封锁协议利用了关系上索引的可用性,该协议运作如下:
- 每个关系至少有一个索引。
- 首先在关系的一个或多个索引上找到元组,事务才能访问元组。
全表扫描被看作一个索引上所有叶节点的扫描。 - 查找的事务需在其要访问的所有索引叶结点上获得共享锁。
- 没更新r的索引前,事务不能插入,删除,更新r的元组。
事务需获得插入,删除,更新涉及的索引叶结点上的排他锁。 对插入,删除.涉及的叶结点是 包含插入后,删除前元组的搜索码值的叶结点。 对更新,涉及的叶结点是 包含修改前,修改后元组搜索码值的叶结点。 - 元组照常获得锁。
- 需遵循两阶段封锁协议。
关系上的插入和删除操作都要检查是否满足谓词。若满足,则存在锁冲突,插入和删除操作要等待直到谓词锁被释放。对于更新操作,元组的初始值和最终值都要检查是否满足谓词。 这些冲突的插入、删除和更新操作影响谓词选中的元组集合,因此不能运行它们与已获得(共享的)谓词锁的查询并发执行。称上述的协议为谓词锁。
谓词锁在实践中不采用,因为相比较索引封锁机制,它的实现代价很大,但又不能带来显著的额外好处。
实践的弱一致性级别
二级一致性
二级一致性的目的是在不必保证可串行性的前提下放在发生级联中止。
二级一致性的封锁协议的两类锁:共享锁(S),排他锁(X)。 对共享锁,可随时获得,释放。 对排他锁,可随时获得,只能在事务终止或提交时释放。
不保证可串行性。
不可重复读(事务两次读取,产生不同结果)。 读到的数据必是已经提交的。
游标稳定性
游标稳定性是二级一致性的一种形式,它为利用游标对关系中的元组进行迭代的程序而设计的。
它不封锁整个关系,游标稳定性保证:
- 正被迭代处理的元组被加上共享锁。
- 被更改的元组被加上排他锁,直到事务提交。
保证了二级一致性,不要求两阶段封锁,没有保证调度的可串行性。
实践中游标文档性用于频繁访问的关系,以作为一种提高并发性和改善系统性能的方法。
跨越用户交互的并发控制
几种并发控制方法:时间戳、有效性检验、快照隔离。
-
事务拆分 将涉及用户交互的事务划分为两个或多个事务,保证划分后,无事务跨越用户交互。 -
不做读有效性检查的乐观并发机制 元组有版本号,元组创建时为0。 1.读元组作为独立数据库事务。 2.对每个更新的元组,以原子方式执行如下: 检查当前的版本号是否等于首次读元组事务时的版本号。 如果匹配,执行元组更新,版本号+1。 如果不匹配,事务终止.回滚执行的所有更新。 如果对所有更新元组版本号检查均匹配,事务提交。 该机制的一个诱人的特点是它可以轻松地实现在数据库系统顶层。
学习参考资料:
《数据库系统概念》第6版
|