Jim Gray - Transaction processing: concepts and techniques
http://research.microsoft.com/~gray/
事务概念
事务定义
- 事务是由一系列操作序列构成的程序执行单元,这些操作要么都做,要么都不做,是一个不可分割的工作单位。
事务特性(ACID)
* 原子性(Atomicity)
事务中包含的所有操作要么全做,要么全不做,
原子性由恢复机制实现。
* 一致性(Consistency)
事务的隔离执行必须保证数据库的一致性
事务开始前,数据库处于一致性的状态;事务结束后,数据库必须仍处于一致性状态
数据库的一致性状态由用户来负责
如银行转账,转账前后连个账户金额之和应保持不变(意大利香肠)
* 隔离性(Isolation)
系统必须保证事务不受其它并发执行事务的影响
对任何一对事务T1, T2, 在T1看来,T2要么在T1开始之前已经结束,要么在T1完成之后再开始执行
隔离性通过并发控制机制实现
* 持久性(Durability)
一个事务一旦提交之后,它对数据库的影响必须是永久的
系统发生故障不能改变事务的持久性
持久性通过恢复机制实现
显示事务 - 以begin transaction开始,以commit或rollback结束
隐含事务 - 事务自动开始,直到遇到commit或rollback时结束
自动事务 - 每个数据操作语句作为一个事务
平面事务 - 一层结构 begin tran.....commit
平面事务的缺点:不能部分回滚
嵌套事务 - 事务中包含事务
分布式事务
N个T,N个DBMS
事务调度
事务的执行顺序称为一个调度,表示事物的指令在系统中执行的时间顺序
一组事务的调度必须保证
包含了所有事务的操作指令;
一个事务中指令的顺序必须保持不变。
串行调度
并行调度
可恢复调度
事务的恢复:
一个事务失败了,应该能够撤销该事务对数据库的影响。如果有其它事务读取了失败事务写入的数据,则该事务也应该撤销
事务隔离性级别:
读脏数据
不能重复读
发生幻象
SQL中隔离性级别的定义
serializable
repeatable read
read committed
read uncomitted
事务的调度的正确性的标准是:
一个并发执行的调度应该等价于这组事务的一个串行的调度
冲突可串行化
* 冲突指令
当两条指令是不同事务在相同数据项上的操作,并且其中至少有一个是write指令时,
则称这两条指令是冲突的
非冲突指令交换次序不会影响调度的最终结果
* 冲突等价
如果调度S可以经过一系列非冲突指令交换转换成调度S‘,则称调度S与S‘是冲突等价的
冲突可串行化
当一个调度S与一个串行调度冲突等价时,则称该调度是冲突可串行化的
冲突可串行化判定
优先图(precedence gragh)
** 视图可串行化 **
* 视图等价
考虑关于某个事务集的两个调度S, S‘,若调度S,S‘满足以下条件,则称它们是视图等价的:
1. 对于每个数据项Q,若事务Ti在调度S中读取了Q的初始值,
那么Ti在调度S‘中也必须读取Q的初始值。
2. 对于每个数据项Q,若事务Ti在调度S中执行了read(Q),并且读取的值是由Tj产生的,
那么Ti在调度S‘中读取的Q值也必须是由Tj产生的。
3. 对于每个数据项Q,若在调度S中有事务执行了最后的write(Q),
则在调度S‘中该事务也必须执行最后的write(Q)。
* 视图可串行化
如果某个调度视图等价于一个串行调度,则称该调度是视图可串行化的
冲突可串行化调度一定是视图可串行化的
存在视图可串行化但非冲突可串行化的调度
* 带标记的优先图的构造
设调度S包含了事务{T1, T2, ..., Tn},设Tb, Tf是两个虚事务,其中Tb为S中所有write(Q)操作,
Tf为S中所有read(Q)操作。在调度S的开头插入Tb,在调度S的末尾插入Tf,
得到一个新的调度S‘。
1. 如果Tj读取Ti写入的数据项的值,则加入边Ti--0->Tj
2. 删除所有关联无用事务的边。如果在优先图中不存在从Ti到Tf的通路,则Ti是无用事务。
3. 对于每个数据项Q,如果Tj读取Ti写入的Q值,Tk执行write(Q)操作且Tk!=Tb,则:
1) 如果Ti = Tb且Tj != Tf,则在带标记的优先图中插入边Tj -0-> Tk
<Tk, Tb, Tj> (X不允许) <Tb, Tk, Tj> (X不允许) <Tb, Tj, Tk > (允许)
2) 如果Ti != Tb 且Tj = Tf,则在带标记的优先图中插入边Tk -0-> Ti
<Ti, Tf, Tk > (不允许) , <Ti, Tk, Tf > (不允许) , <Tk, Ti, Tf > (允许) ,
3) 如果Ti != Tb 且 Tj != Tf,则在带标记的优先图中插入边Tk -p-> Ti与 Tj -p-> Tk。
其中p是一个唯一的,在前面边的标记中未曾用过的大于0的整数。
<Ti, Tk, Tj > (不允许) , <Tk, Ti, Tj > (允许) , <Ti, Tk, Tf > (允许)
Q: 这个虚拟事务是干啥的?
图:出现闭环,不是视图可串行化。
图:没有出现环路,是视图可串行化的。??
封锁的定义
封锁就是一个事务对某个数据对象加锁,取得对它一定的控制,
限制其它事务对该数据对象使用。
要访问一个数据项R,事务Ti必须先申请对R的封锁,如果R已经被事务Tj加了不相容的锁,
则Ti需要等待,直至Tj释放它的封锁。
封锁的类型
排他锁 (X锁,eXclusive lock)
- 写锁
申请对R的排他锁:lock-X(R)
共享锁 (S锁,Share lock)
- 读锁
申请对R的共享锁:lock-S(R)
** 两阶段封锁协议 **
Two-Phase Locking Protocol
* 两阶段封锁协议内容
增长阶段(Growing Phase) - 事务可以获得锁,但不能释放锁
缩减阶段(Shrinking Phase) - 事务可以释放锁,但不能获得锁
示例:
lock-S(A)... lock-S(B)...lock-X(C)...
unlock(A)...unlock(C)...unlock(B)...
遵从两段锁协议
lock-S(A)...unlock(A)...lock-S(B)...
lock-X(C)...unlock(C)...unlock(B)...
不遵从两段锁协议
* 两阶段封锁协议
* 封锁点:事务获得其最后封锁的时间
* 事务调度等价于和它们的封锁点顺序一致的串行调度
* 令{T0, T1, ..., Tn}是参与调度S的事务集,如果Ti对数据项R加A型锁,
Tj对数据项R加B型锁,且comp(A,B)=false,则称Ti先于Tj,记做Ti->Tj,
得到一个优先图
* 设ti是Ti的封锁点,若Ti -> Tj,则ti < tj
* 若{T0, T1, ..., Tn} 不可串行化则在优先图中存在环,不妨设为T0->T1->...->Tn->T0,
则t0<t1<...<tn<t0,矛盾
* 保持到事务结束时才释放的锁称为长锁
* 在事务中途就可以释放的锁称为短锁
两阶段封锁+短X锁+短S锁
(SQL中隔离性级别的定义
serializable
repeatable read
read committed
read uncomitted)
repeatable read - 只有X和S锁都是长锁的时候,才能保证“不可重复读”这个条件
* 锁转换
* 带有锁转换的两段锁协议
* 增长阶段
可获得lock-S
可获得lock-X
可将lock-S升级为lock-X(upgrade)
* 缩减阶段
可释放lock-S
可释放lock-X
可将lock-X升级为lock-S(downgrade)
封锁粒度
* 封锁对象
属性值,属性值集合,元组,关系,某索引项,整个索引,整个数据组,物理页,块
* 封锁粒度大,则并发度低,封锁机构简单,开销小
* 封锁粒度小,则并发度高,封锁机构复杂,开销高。
* 理想的情况是只封锁与规定的操作有关的数据对象,这些数据对象称作事务的完整性相关域。
* 数据对象从大到小有一种层次关系,当封锁了外层数据对象时也就意味着同时封锁了它的
所有内层数据对象。
数据库 -> 段 -> 关系 -> 元组
* 意向(预约)粒度
* 在分层封锁中,封锁了上层节点就意味着封锁了所有内层节点。如果有事务T1对某元组加了
S锁,而事务T2对该元组所在的关系加了X锁,因而隐含的X封锁了该元组,从而造成矛盾。
* 引入意向锁I (Intend):当为某节点加上I锁,表明其某些内层节点已发生事实上的封锁,
防止其它事务再去显式封锁该节点。
* I锁的实施是从封锁层次的根开始,依次占据路径上所有节点,
直至要真正进行显式封锁的节点的父节点为止。
* 相容矩阵
I与I相容
S与S相容
其他不相容
IS锁
如果对一个数据对象加IS锁,表示它的后裔节点拟(意向)加S锁
例如,要对元组加S锁,则首先要对关系和数据库加IS锁
IX锁
如果对一个数据对象加IX锁,表示它的后裔节点拟(意向)加X锁
例如,要对元组加S锁,则首先要对关系和数据库加IX锁
更精细的相容矩阵
IS IX S X
在实际的数据库中,IS锁是不存在的??
SIX锁
如果对一个数据对象加SIX锁,表示对它加S锁,再加IX锁
* 例如对某个表加SIX锁,则表示该事务要读整个表(对该表加S锁),
同时会更新个别元组(对该表加IX锁)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN TRAN
SELECT * FROM bigtable
UPDATE bigtable
SET col = 0
WHERE keycolumn = 100
码范围锁定
* 码范围锁定原理
* 码范围锁定原理解决了幻像读并发问题
* 码范围锁覆盖单个记录以及记录之间的范围,
可以防止对事务访问的记录集进行幻像插入或删除。
* 码范围锁通过覆盖索引行和索引行之间的范围来工作(而不是锁定整个基础表的行)。
因为第二个事务在该范围内进行任何行插入,更新或删除操作时均需要修改索引,
而码范围锁覆盖了索引项,所以在第一个事务完成之前会阻塞第二个事务的进行。
** 锁的实现 **
* 锁管理器
* 事务向锁管理器发送封锁的申请和释放请求
* 锁管理器维护一个锁表记录锁的授予情况和处于等待状态的封锁请求
* 锁表
* 锁表一般作为内存中的hash表,按照被封锁对象的名字建立索引
* 黑矩形表示已被授予的锁,白色表示等待的封锁请求
* 锁表同时记录锁的类型
* 新的封锁请求加到对应请求队列的末尾,当封锁请求与前面的锁相容时被批准
* 释放封锁时请求从队列中删除并检查后续请求是否满足
* 如果事务放弃,所有授予的和等待的锁请求都被删除
* 为提高效率,锁管理器会记录每个事务持有锁的情况
* 如果看待锁
* 封锁资源
* 锁管理器对资源一无所知,它只是“memcmp()”
* 锁资源格式
资源类型 数据库ID 资源详细数据
Object ID - 数据对象
File#: Page# - 封锁一个页面
File#: Page#: Slot on Page - 封锁一条元组
* RID: 8字节(File#, Page#, Slot#)
除非删除或移到其他地方,否则RID保持不变;如果删除元组,RID可以重用
RID可以作为封锁资源
* 聚集索引
行可以由唯一的聚集码标识
聚集码可以作为封锁资源
* 二级索引
码和位置可以作为封锁资源
* 锁升级
* 死锁
* 死锁发生的条件
* 解决死锁的方法
* 预防死锁
* 预先占据所需的全部资源,要么一次全部封锁要么全部封锁
缺点:难于预知需要封锁哪些数据并且数据使用率低
* 所有资源预先排序,事务按规定顺序封锁数据
* 使用抢占与事务回滚,给每个事务分配一个时间戳,若事务T2所申请的锁已经被T1持有,
可以比较T1与T2的时间戳,来决定是否会滚T1,并将T1释放的锁授予T2。
Q:时间戳晚的回滚?
若事务T2所申请的锁已经被T1持有,如果T2的时间戳比T1早,则回滚T1,
让T2执行,之后再执行T1
——即:时间早的先执行。时间戳晚的回滚,让步于早的。
(在编写事务的时候可以按照避免方法来写,比如2个事务同时先更新A表,然后更新B表,
这样只会发生等待,而不会发生死锁。)
* 死锁检测和恢复
* 超时法
如果等待封锁的时间超过限时,则撤销该事务
* 等待图法
** 活锁(live lock) **
* 可能存在某个事务永远处于等待状态,得不到执行,称之为活锁(饿死)
* T2持有对R的S锁,T1申请对R的X锁,则T1必须等待T2释放S锁;若在T2完成之前有
T3申请对R的S锁,则可以获得授权封锁,于是T1必须等待T2,T3释放S锁
* 避免活锁的策略是遵从“先来先服务”的原则,按请求封锁的顺序对各事务排队;
当事务Ti对数据项R加M型锁时,获得封锁的条件是:
1. 不存在在R上持有与M型锁冲突的锁的其他事务 - (类型冲突)
2. 不存在等待对R加锁且先于Ti申请加锁的事务 - (时间)
Note: FIFS - First In First Service
* 阻塞
例子:某行加了X锁,导致第二个连接加IX锁请求受阻。加一条索引就不会阻塞了
CREATE INDEX idx1 ON test
* 数据库故障
* 事务故障
* 指事务的运行没有到达预期的终点就被终止
* 非预期故障
1. 不能由事务程序处理的
如:运算溢出,发生死锁而被选中撤销该事务
* 可预期故障
1. 应用程序可以发现的事务故障,并且应用程序可以让事务回滚
如:转账时发现账面金额不足
* 系统故障
* 软故障(soft crash):在硬件故障,软件错误的影响下,虽引起内存信息丢失,
但未破坏外存中数据
如:CPU故障,突然停电,DBMS,OS,应用程序等异常终止
* 介质故障
* 硬故障(hard crash):又称磁盘故障,破坏外存上的数据库,
并影响正在存取这部分数据的所有事务
如:磁盘的磁头碰撞,瞬时的强磁场干扰
* 恢复的定义
* 恢复是把数据库从错误状态恢复到某一正确状态的功能,从而确保数据库的一致性
* 恢复的基本原理是冗余,即数据库中任一部分的数据可以根据存储在系统别处的冗余数据来重建
* 转储
* 将数据库复制到磁带或另一个磁盘上保存起来的过程。这些备用的数据称为后备(后援)副本
* 静态转储
转储期间不允许对数据库进行任何存取,修改活动
* 动态转储
转储期间允许对数据库进行任何存取或修改
* 海量转储
每次转储全部数据库
* 增量转储
每次只转储上次转储后更新过的数据
* 日志
* 日志文件是以事务为单位用来记录数据库的每一次更新活动的文件,由系统自动记录
* 日志内容包括:记录名,旧记录值,新记录值,事务标识符,操作标识符等
* 事务Ti开始时,写入日志:<Ti start>
* 事务Ti执行write(X)前,写入日志:<Ti, X, V1, V2>,V1是X更新前的值,V2是X更新后的值
* 事务Ti结束后,写入日志:<Ti commit>
例子:见截图。
* 事务分类
* 圆满事务
日志文件中记录了事务的commit标识
* 夭折事务
日志文件中只有事务的Begin transaction 标识,无commit
* 基本的恢复操作
* 对圆满事务所做过的修改操作应执行redo操作,
即重新执行该操作,修改对象被赋予新记录值。
redo = redo两次方
* 对夭折事务所做过的修改操作应执行undo操作,
即撤销该操作,修改对象被赋予旧记录值
* 先写日志的原则(WAL)
* 对于尚未提交的事务,在将DB缓冲区写到外存之前,
必须先将日志缓冲区内容写到外存去
* 日志记录将要发生何种修改
* 写入DB表示实际发生何种修改
* 如果先写DB,则可能在写的中途发生系统崩溃,导致内存缓冲区内容丢失,
而外存DB处于不一致状态,由于日志缓冲区内容已破坏,导致无法对DB恢复。
* 事务故障恢复
* 撤销事务已对数据库所做的修改
* 措施
1. 反向扫描日志文件,查找该事务的更新操作
2. 对该事务的更新操作执行逆操作,即将事务更新前的旧值写入数据库
3. 继续反向扫描日志文件,查找该事务的其他更新操作,并做同样处理
4. 如此处理下去,直至读到此事务的开始标识,事务的故障恢复就完成了
* 系统故障恢复
* 不一致状态原因
1. 未完成事务对数据库的更新已写入数据库
2. 已提交事务对数据库的更新未写入数据库
* 措施
1. 正向扫描日志文件,找出圆满事务,记入重做队列;
找出夭折事务,记入撤销队列
2. 反向扫描日志,对撤销队列中事务Ti的每一个日志记录执行undo操作
3. 正向扫描日志文件,对重做队列中事务Ti的每一个日志记录执行redo操作
例子:发生系统故障恢复时,先undo后redo,才能保持数据库一致性。
* 介质故障恢复
* 磁盘上数据文件和日志文件遭到破坏
* 措施
1. 装入最新的数据库后备副本,使数据库恢复到最近一次转储时的一致性状态。
2. 装入相应的日志文件副本,重做已完成的事务
* 检查点(Checkpoint)
* 系统恢复时,需要搜索整个日志文件决定事务是夭折的还是圆满的
1. 搜索过程太耗时
2. 大多数需要被重做的事务其更新已经写入了数据库中。尽管对它们重做不会造成不良后果,
但会使恢复过程变得更长。
* 生成检查点
1. 将当前位于主存的所有日志记录输出到稳存上
2. 将所有修改了的缓冲块输出到磁盘上
3. 将一个日志记录<checkpoint>输出到稳存
* 检查点作用
* 避免故障恢复时扫描整个日志文件
* 避免redo两次
保证检查点之前日志与数据库内容一致
* 对在检查点前提交的事务不必对其执行redo操作
* 检查点
* 最小恢复LSN(MinLSN),它是下面这些LSN中的最小LSN:
检查点起点的LSN
最旧的活动事务起点的LSN
* 检查点的生成
检查点由系统自动。自动检查点的时间间隔基于日志内的记录数而非时间