事务管理
1 概述
1.1 ACID
略
1.2 事务和调度
事务是一个操作序列
事务的操作包括:
- 读 / 写
- Commit / Abort
两个假设:
- 事务间不能交换消息
- 数据库是由独立obj构成的
Schedule 是一组的事务的操作列表,且来自同一事务的操作的次序是相同的
包含Commit和abort的称为Complete Schedule
如果不同事务的操作之间不交叉,我们称为Serial Schedule
1.3 Concurrent Execution
1.3.1 可串行化
一个serializable schedule 指的是该schedule与某个serial schedule的最终结果一样
尽管是serial schedule,不同的执行顺序也会带来不同的结果,因此DBMS不确保会生成哪一个结果
DBMS 也有可能使用不可串行化的调度,原因:
- DBMS的并发控制方法不是可串行化,但等价于某种可串行化调度
- SQL允许程序员使用非串行化的调度
1.3.2 三种错误
- WR dirty read, reading uncommitted
- RW unrepeatable read
- WW blind write, overwriting uncommitted, lost update
1.3.3 包含Abort的Schedule
重新定义 : 一个serializable schedule 指的是(对于已提交的事务,)该schedule与某个serial schedule的最终结果一样
上述的定义依赖于所有与被abort的事务有关的操作都被undo,然而在有的情况下是不能实现的,例如:
INITIAL : A = 0;
T1 : A = 5;
T2 : Read(A);
T2 : Commit;
T1 : Abort;
上述的调度就被称为unrecoverable schedule
recoverable schedule 指的是只有当与某个事务所读的数据相关的所有事务都提交了,该事务才提交
如果某个schedule中事务仅仅读已提交了的数据,那么该schedule就是 avoid cascading abort 的调度,意思是该调度处理abort事务时,不需要abort其它的事务。
如果在一个调度中的事务写过的值,在该事务commit或者abort之前,不被其他事务所读或者覆盖写,那么这个调度被称为 strict schedule。Strict schedule 是 recoverable 的,也是 avoid cascading abort 的
另外还有一个问题:
INITIAL : A = 0;
T1 : A = 5;
T2 : A = 6;
T2 : Read(A);
T1 : Abort;
T2 : Commit;
这个时候, T2其实并没有用到T1修改后的值,但是T2后续的commit已经没有作用了。我们可以使用Strict 2PL的方法解决上述问题。
1.4 基于锁的并发控制
1.4.1 Strict 2PL
两条规则:
事务 Read 的时候申请 Shared Lock, Modify的时候申请 Exclusive Lock
事务完成后,释放该事务的所有锁
1.4.2 Deadlock
最容易的方法是 timeout 机制,后面再详细讲解解决死锁的方法
1.5 性能
锁机制用于解决事务间的冲突,一般来说有两种机制:blocking 阻塞 和 aborting 中止
阻塞的事务可能会持有一些别的事务正在等待的锁,可能导致死锁现象
中止的方法会浪费了该事务已经完成的工作。
在实际中,延迟主要源于blocking,因为实际上发生死锁,或者事务abort的概率很小
少量的事务不太可能导致冲突,此时吞吐量和事务数成正比
随着事务量的不断增加,存在一个点,当事务量增加会导致吞吐量下降,我们称该系统在该点上 thrash。当我们开始发现系统 thrash 后, 应该尝试减少允许并发的事务数
增加吞吐量可以通过三个方式:
尽可能少地加锁,降低两个事务需要同一个锁的几率
减少事务的持锁时间
减少 hot spot,hot spot 是经常被访问的数据库对象
1.6 SQL中的事务操作
1.6.1 创建和结束
当用户执行访问数据库的语句时,事务自动启动
事务被一个 commit 或者 rollback 指令结束
对于长时间运行的事务,SQL有两个支持特性:
savepoint,允许在事务中指定一个点,可以选择性地回滚该点以后(包括创建savepoint的操作)的所有操作。
chained transaction 允许提交和回滚事务,且立刻开启另一个事务
1.6.2 WTF should we lock?
我们需要思考,DBMS应该将什么东西锁住呢?比如说,我们可以考虑锁住更小的对象,例如:
T1 : Select S.rating
From Sailor S
Where S.rating = 8
T2 : Update Sailor
Set age = 20
where sid = 100
我们也可以对 Sailor 表加锁,但是这样的效率很低。更好的做法是对 Sailor 的每一条记录分别加锁。
上述就是 DBMS 的不同 granularities,一般使用 row-level lock
我们再来讨论一个新的问题,如果 T1 正在查找出所有 rating = 8 的记录并加锁,此时来了一个 T3 事务,往数据库中添加了一条新的 rating = 8 的记录,这时候 T1 再次执行将会得到不同的结果。
上述的现象称为 phantom problem : 事务两次从数据库中提取元素,得到两个不一样的结果
为了避免该现象,必须保证所有可能的行都被加锁了,一个做法就是对表加锁,但是会带来并发性的下降,后面再详细讲解。
1.6.3 SQL中的事务特性
SQL 的三种事务特性:
access mode
diagnostics size ,决定被记录下来的错误情况的个数
isolation level
if access mode == READ ONLY:
事务不被允许修改数据库
else if access mode == READ WRITE:
事务可以进行 INSERT, DELETE, UPDATE, CREATE等操作
四种 isolation level:
- READ UNCOMMITTED : 读之前不获取 Shared Lock,SQL 对于这种模式限制了所有的修改行为,也就是 READ ONLY
- READ COMMITTED : T 只读已提交的事务,在 T 事务完成前 T 写的值仅为 T 所更改 (读的值有可能被其他 Xact 改变,因为其拿到了 Shared Lock 后很快就释放了, 因此有可能被改变)
- REPEATABLE READ : T 只读已提交的事务,在 T 事务完成前 T 读或者写的值仅为 T 所更改,会有 Phantom 现象 (只做了 obj locking,没有做 index locking ,后面会讲)
- SERIALIZABLE : T 只读已提交的事务,在 T 事务完成前 T 读或者写的值仅为 T 所更改,如果 T 根据条件读取一些值,则在 T 完成之前这些值都不会改变(也就是避免了 phantom 现象)
Level | Dirty Read | Unrepeatable Read | Phantom |
---|---|---|---|
READ UNCOMMITTED | Maybe | Maybe | Maybe |
READ COMMITTED | No | Maybe | Maybe |
REPEATABLE READ | No | No | Maybe |
SERIALIZABLE | No | No | No |
DBMS 一般推荐使用 SERIALIZABLE,但是比如求所有值的平均值这种操作,可以使用 READ COMMITTED 甚至 READ UNCOMMITTED,因为一两个值并不会太影响最终的结果。
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE READ WRITE
Initial 的设定为 :SERIALIZABLE READ WRITE
1.7 崩溃恢复
1.7.1 Stealing Frames and Forcing Pages 偷帧、强制写页
在事务提交之前,事务修改的、存放在内存中的值可以被写回磁盘吗? 如果允许这种操作,则称为 steal 。
事务提交之后,该事务所修改了的、存放在内存中的页应该被立刻写回磁盘吗? 如果这么做,则称为 force。
使用 no-steal 则不需要 undo abort 事务的操作。但是这种做法基本不能实现,因为内存不可能提供这么多数据页
使用 force 则会导致大量的 page IO,因为可能有一些经常被使用的页会被重复多次写回磁盘又从磁盘读出。
因此大部分的系统都是使用 steal,no-force 的方法
1.7.2 正常运行中恢复管理程序的执行
log 被存放在 stable storage 中,确保不会崩溃,一般会有好几份,分散在不同的地方。
写 log 的操作需要比实际的操作更早,称为 WAL,Write-Ahead Log
DBMS 每过一段时间就会强制将内存中的页写入磁盘中
checkpoint,存储 active Xact 和 dirty pool pages 的信息,也是一种减少恢复时间的方法
1.7.3 ARIES 算法
一种恢复算法,使用 steal,no-force的方法
三个阶段:
Analysis : identify 在 Crash 发生的时候,内存中的 dirty pages 和 active Xact
Redo : 从 log 的某一个点开始,重复所有的操作,恢复崩溃发生时数据库的状态
Undo : undo 那些没有 Commit 的 Xact 的操作,因此数据库就只留下了 committed Xact 的状态了
1.8 总结
- 解释一下什么是ACID?
- 解释一下什么是事务?什么是调度?什么是完全调度?什么是串行化调度?
- 解释一下什么是可串行化调度?可恢复调度?可避免级联中止调度?严格调度?
- 解释操作同一对象可能会发生哪些冲突?并各自解释一下?
- 解释一下 Strict 2PL?
- 基于锁的并发控制有什么开销?解释一下阻塞方法和中止方法的开销?
- 解释一下 Thrashing?
- 如何增加吞吐量?
- SQL中如何创建和结束事务? 解释 save-point,链事务?
- 如何确定锁的粒度? 解释幻影问题? 对性能的影响?
- 如何控制 SQL 中的事务特性? 存取模式和隔离级别
- 如何实现不同的隔离级别? 用锁实现
- 偷帧和强制写页是什么?
- 恢复操作包括哪几步?如何减少恢复操作的时间?
- 怎么使用 log 进行事务回滚或者崩溃恢复?
2 并发控制
2.1 2PL
两个操作对同一个数据对象进行操作,且至少其中一个操作为写,那么这两个操作冲突
对于非冲突操作调换位置不会影响调度的结果,只有对冲突操作调换位置才会影响调度的最终结果
因此,如果两个调度包含相同的事务,且所有冲突操作的顺序也相同,那么这两个调度被称为 conflict equivalent
如果两个调度是 conflict equivalent,那么这两个调度的实际效果是一样的
如果一个调度与某个serial schedule是conflict equivalent的话,那么这个调度被称为conflict serializable
(这里我们回顾一下,serializable schedule指的是最终结果与某个serial schedule相同的调度。因此可以看出,conflict serializable 一定是 serializable,因为conflict serializable指的是可以通过调换非冲突操作最后达到某个serial schedule,因此最终结果一定与某个serial schedule相同。这里有点tricky,可以细想)
当然,有的serializable schedule并不是conflict serializable,例如:
T1 : R(A);
T2 : W(A);
T1 : W(A);
T3 : W(A);
上述调度的结果等于T1,T2,T3,因此是serializable的,但不是conflict serializable,因为两个W的操作无法调换。
将一个调度中可能的冲突记录下来,称为precedence graph优先图,或者叫做serialiability graph串行化图
node 是一个已提交的事务,Ti -> Tj 指的是Ti先执行,且与Tj的某个操作冲突
严格2PL只允许conflict serializable的调度,有以下两个结论:
- 如果调度是conflict serializable,那么它的优先图是无环的;
- 严格2PL的调度的优先图都是无环的。
严格2PL的一个变型是2PL,允许事务在commit或者abort之前释放锁,2PL中的事务一旦释放锁之后不能请求额外的锁。
因此每一个2PL的事务都有两个阶段,请求锁的上升阶段和释放锁的下降阶段。
2PL同样也是保证了只允许conflict serializable的调度
严格调度指的是,某个事务写的值,不被其他事务读或修改,直到该事务提交或者abort
严格调度是可恢复的,不需要级联中止的
// 概念实在是太多了,记不住回头看一看就好
Strict 2PL保证了每一个调度都是严格且冲突可串行化的,理由是严格2PL只有在结束时才释放锁,也就是符合严格的定义
2.1.1 View Serializability
冲突可串行化并不是必要的,往往我们只需要做到view serializabiliy
两个包含相同事务的调度被称为view equivalent,如果:
- 如果事务T读到A的初始值,那么在另一个调度中也一样;
- 如果事务Ti读到了Tj所写的A的值,那么在另一个调度中也一样;
- 如果事务T是最后写A的值的,那么在另一个调度中也一样;
如果调度view equivalent to 某个serial schedule,那么该调度是view serializable
任何观测可串行化的调度,如果不是冲突可串行化,那么它含有blind write
2.2 锁管理
lock manager 管理一个 lock table,是一个hash表,键为data object identifier
lock table 的项可以是一个记录,一页等等,取决于DBMS,包含了对该obj拥有锁的事务数量,锁的性质,指向请求锁的等待列表
还管理了一个事务表,每一项是事务的描述信息,以及一个指向事务所拥有的锁列表的指针,请求锁之前需要检查这个列表,以确保事务不会对同一个锁请求两次
2.2.1 加锁解锁的实现
if 请求共享锁:
if 请求队列为空 and 对象无互斥锁://注意这里有一个请求队列为空的要求,后面有解释为什么
同意锁请求;
修改该数据对象状态为共享锁;
拥有该对象的事务数量++;
elif 请求互斥锁:
if 没有事务拥有该对象:
同意锁请求;
更新锁表;
else:
添加到该对象的请求队列中;
挂起该请求事务;
值得注意的一点:
T1 : R(A); // T1获得A的共享锁
T2 :W(A); // T2进入等待序列
T3 :R(A); // T3进入等待序列!!!而非获得共享锁,这样的做法可以保证 T2 不会被饿死(大量请求共享锁的事务导致请求互斥锁一直等待)
加锁解锁的过程需要使用操作系统的同步机制(如,信号灯semaphore),例如T1和T2事务同时请求互斥锁,都知道了当前没有别的事务在使用,因此两个事务都得到了互斥锁,导致错误,具体分析可以看我的关于操作系统的复习专栏。
Latches,Convoys
DBMS还支持Latches,在读写之前设置latch闭锁,确保物理读写是原子性的,否则两个读或者写操作可能会发生磁盘页冲突,物理读写结束后立即释放latches
DBMS的事务调度与操作系统对进程的调度可能会相互影响导致护卫现象,指的是大部分的CPU周期都被用在了进程转换上面了。
如果某个持有(被所有事务频繁使用的)锁的事务被操作系统挂起,在这个事务被恢复之前,会有一系列的请求该锁的事务队列,称为convoy,convoy一旦建立后会持续很长的时间。
2.3 锁转换
事务对已经获得共享锁的对象请求转换为互斥锁,例如SQL的更新语句,可能会先对SQL的每一行加共享锁,如果某行满足条件,则需要对该行请求转换为互斥锁
如果没有其他事务拥有此对象,可以立即转换;否则,将该转换锁的请求放在等待队列的最前面
原因是,此对象已经拥有了共享锁了,如果放在队列后面且队列中有互斥锁请求,则会导致死锁。R1 -> W2 -> W1,1事务不结束,2事务也不能开始,1事务的转换请求也永远不能满足
如果两个拥有同一对象的共享锁的事务都请求转换为互斥锁,那么就会发生死锁
一个解决方法是:避免升级锁的操作,一开始先获得互斥锁,一旦确定了该对象不需要互斥锁,则降级为共享锁
更新锁,可以与共享锁共存,但是不能与其他更新锁或者互斥锁共存。一开始可以设置为更新锁,那么其他只读的事务也可以共享该对象,一旦我们确定了不需要更新对象,那么就降级为共享锁。如果我们需要更新对象,首先需要升级为互斥锁,且不会导致上面所说的死锁问题。
2.4 死锁处理
DBMS周期性地检查死锁,锁管理器管理一个wait-for-graph用来检测死锁,节点表示正在进行的事务,Ti -> Tj 表示Ti在等待Tj释放锁
等待图包含可能中止的事务,优先图只有已提交的事务。等待图与优先图的箭头方向相反。
通过中止环中的某一个事务解决死锁,根据各种标准选择被牺牲的事务。
2.4.1 死锁预防
如果死锁不经常发生,上述基于计划的死锁检测效果很好。
如果对锁的竞争较大,那么可以使用基于预防的方案,通过赋予事务优先权值,保证优先权高的事务不用等待优先权低的事务,其中一种优先权值的分配方法是根据时间戳,越早启动的事务优先权值越高。
出现冲突后可以采用下面两种方法:
- 等待-死亡策略:优先权高可以等待,否则直接中止 // 可能出现老事务一直等待新事务的现象
- 受伤-等待策略:优先权高可以直接插队,中止正在进行的事务,否则等待 // 可能出现新事务不断被老事务中止
当事务被中止后,应保证其拥有原来的时间戳,确保原有的优先权
保守2PL,在事务执行时,需要先获得事务所需要的所有锁,否则该事务释放所有拥有的锁,等待时机。在频繁出现死锁的情况下,可以减少锁的平均持有时间,否则会减慢DBMS效率。这种方法一般很少用,因为出现死锁的情况并不常见。
2.5 特殊加锁技术
2.5.1 动态数据库、幻影问题
如果数据库的数据并不是固定不变的,我们必须处理复杂的幻影问题。
如果有新的数据项加入,冲突可串行化不保证为可串行化。
- 如果没有索引,事务需要对所有页加锁且保证没有新的页加入
- 如果有索引,事务对包含符合条件的索引页加锁,如果没有该索引页,则把将有可能出现该索引的页锁起来。这就叫做索引锁
索引锁是predicate locking 谓词锁的一种特殊情况
2.5.2 B+树并发控制
- 树的高层节点仅仅用于搜索,数据存放在叶子节点
- 如果插入操作导致叶子节点分裂并向上传播,则该上节点也应该被锁
搜索的时候,对于节点应该获取共享锁,如果已经获得下一层节点的锁,上一层节点的锁可以被立即释放
一种保守的做法是对所有的节点都加互斥锁,因为分裂节点的操作理论上可能延续到根节点。如果子节点在锁的时候不是满的,则分裂的行为最多只会延续到该节点,而不会往上继续分裂,因此可以释放父节点的锁
对子节点加锁,释放父节点的锁的技术称为lock-coupling,crabbing 锁耦合
在上图中,插入25,前面步骤省略,事务拥有F节点的Slock,拥有H节点的Xlock,因为插入带来了分裂行为,所以需要对F的锁升级为Xlock
如果此时有一个事务拥有F的Slock,且尝试访问H节点,则会导致死锁发生
2.5.3 多粒度锁
可以通过对象树(某节点被锁住,其子节点也被锁住)保证对文件的加锁
多粒度锁还引入了IS准备共享锁,IX准备互斥锁,IS锁与互斥锁冲突,准备互斥锁与S和X都冲突
事务需要在对某节点加S(X)锁之前,先对该节点的所有子孙节点加IS(IX)锁
通常的情况是,事务需要读整个文件,并修改其中的部分记录,也就是说需要获得S锁和IX锁,再对某些记录对象加X锁。因此定义一个SIX锁,等同于获得了S锁和IX锁。
解锁的时候需要按照从叶子到根的顺序释放
确定锁的合适粒度的方法:首先获得细粒度锁(记录级),在该事务获得了一定数量的该粒度锁后,升级为高粒度的锁(页级),称为锁提升Lock Escalation
2.6 不加锁的并发控制方法
2.6.1 乐观并发控制
对于数据访问竞争相对缓和的系统,不需要使用锁协议
事务的三个执行阶段:
- 读 // 从数据库读数据并写入私有工作区
- 验证 // 如果事务决定提交,检查该事务是否与其他食物冲突,如果存在冲突则事务中止,清空私有工作区重新启动
- 写 // 在私有工作区的修改复制到数据库中
事务在验证阶段开始的时候得到一个时间戳,检查时间戳顺序与事务串行顺序是否相同
如果时间戳Ti < Tj,那么:
- Ti在Tj开始前已完成三个阶段,或者;
- Ti在Tj的写阶段前已经结束,且Ti没有写Tj所读的数据对象,或者;
- Ti在Tj读阶段前已经完成了读阶段,且不写任何Tj读写的对象。
第一个条件是串行顺序执行;第二个条件是Tj在Ti正在修改对象时读某些没有被Ti修改的对象,且保证了Ti在Tj写阶段前结束;第三个条件是允许Ti、Tj同时进行写操作,但是由于两个对象没有同样的操作对象(允许读读)
验证一个事务的时候,不允许提交其他事务;在检验其他事务之前,当前处理的事务的写阶段必须完成。
使用临界区critical section 保证最多只有一个事务处于验证或者写状态,尽量使得该阶段越短越好
如果写阶段中需要从私有工作区复制数据出来,会使得临界区阶段时间变长,因此可以是指针的方法,只需要将逻辑指针指向私有工作区的对象即可,无需复制对象
上述的三个验证条件比较保守,Ti不能写Tj读取的对象,然而我们只需要确保Ti在Tj前写了就不会影响了
事务在读阶段告知DBMS正在读的数据,当Ti提交时,检查Tj是否正在读Ti写过的数据,如果有,让Tj在验证阶段死亡或者立刻杀死Tj
2.6.2 基于时间戳的并发控制
为每一个事务分配时间戳,如果已知Ti的ai操作与Tj的aj操作冲突,确保ai在aj之前执行
每一个数据库对象给定一个读时间戳RTS和写时间戳WTS,如果TS(T) < WTS(O),那么这个写操作是在T事务开始后发生的,发生WR冲突需要将T中止。
如果TS(T)> WTS(O),则T可以读,RTS(O)设为Max(RTS(O), TS(T)),RTS(O)的变化是物理操作,开销大
如果T希望写对象,TS(T)< RTS(O),发生RW冲突,重启;
如果 TS(T)< WTS(O),冲突,如果忽略过时写操作成为thomas写
否则,T执行,WTS(O)改为TS(T)
Thomas写
如果TS(T)< WTS(O),当前写操作被O的最近写操作覆盖,T的写操作在O的最近写操作之前进行
时间戳协议不保证可恢复性,多用于分布式数据库系统
2.6.3 多版本并发控制
另一种使用时间戳控制的方式,保证事务在读对象时不用等待,每个数据维护多个具有不同写时间戳的版本
每个对象有一个读时间戳,事务读后更新
事务需要写对象时,保证没有被后发的对象读取。
不甚理解,以后再深入了解
3 崩溃恢复
3.1 ARIES简介
steal、no-force的恢复算法,三个阶段:
- 分析 : 找出内存池中的dirty pages,以及崩溃时的活跃事务
- Redo : 重复所有的操作,从log中某一个点开始
- Undo : undo 没有提交的事务的操作
三个原则:
- WAL,先写LOG
- Redo时重复历史操作
- undo时也需要Log
3.2 LOG
log tail 是离当前最近的部分,存储在内存中,每隔一段时间就会被输出到稳定存储
每一个log记录都有一个独一的log sequence number LSN,数据库中的每一页都包含最近一次对该页的改变的操作的LSN,pageLSN
下面的操作都需要写log:
- 更新页 : 修改某页后,添加更新类型的log,pageLSN也要修改
- 提交 : 事务提交时,强制写一个commit类型的log,包含了事务的ID。也就是说,log立刻被写回稳定内存中。一些其他步骤,例如删除事务表中的项等,在提交log后进行
- 中止 : 添加一个中止类型、包含事务ID的log,且开始执行Undo
- 结束 : 在中止和提交事务的其他操作结束后,添加一条包含事务ID的结束log
- Undo an update : 当事务被回滚时,取消更新log的操作,写入CLR补偿日志记录
每条log都包含 : preLSN,transID 和 type,某个事务的log集合构成一个链表,通过preLSN可以快速获得前一条记录,transID存储该事务的ID,type记录log的类型,其他字段与类型相关
更新日志记录
附加字段有:pageID:被修改的页ID;length:in bytes;offset;before-image:更新前的数据;after-image:更新后的数据
CLR
包含一个称为undoNextLSN的字段,记录下一个需要被Undo的LSN
CLR 不会被Undo,反做写入的CLR数目不超过崩溃时对应事务的更新记录数目
CLR写入稳定存储后,操作还没有写入磁盘的时候发生崩溃,CLR的反做操作也会被再次执行
3.3 其他相关数据结构
事务表:每个当前事务都有一个数据项,包括事务ID,状态,LastLSN(该事务最新log的LSN)
脏页表:每一项代表一个内存池中的脏页,包括recLSN(第一个对该页修改的LSN)
3.4 WAL
在把某页数据写入磁盘前,必须先把该页对应的log的记录强制写入稳定存储
3.5 检查点
通过定期设置检查点,可以减少崩溃的恢复工作量
设检查点的三个步骤:
- 写入表示检查点开始的begin_checkpoint记录;
- 构造end_checkpoint记录(包括事务表和脏页表);
- end_checkpoint写入稳定存储后,将master记录(包含begin_checkpoint的LSN)写入稳定存储的某个地方
end_checkpoint记录在构建的时候,DBMS继续执行事务,我们唯一能保证的是写入的事务表和脏页表跟begin_checkpoint的时候一样。这样的检查点称为fuzzy checkpoint模糊检查点,不需要系统暂停所以代价不高
另一方面,这种检查点受脏页表中最早的recLSN限制,因为要从该LSN开始redo
3.6 崩溃恢复
3.6.1 分析阶段
- 确定Redo的开始点;
- 找出崩溃时缓冲池的脏页的超集;
- 确定崩溃时正在执行的、需要被取消的事务
检查离当前最近的begin_checkpoint,并使用紧接着的end_checkpoint的脏页表和事务表,初始化,然后正向扫描日志:
- 如果碰到T的结束log,需要把T从事务表中移除;
- 如果碰到T的log(不包括end log),但是事务表中不存在该事务T,则需要把该事务添加到事务表中,令lastLSN为这条记录的LSN,如果记录为commit记录则status设为C,否则设为U(将要被undo);
- 如果日志会影响页P,且能够重做,而P又不在脏页表中,则需要向脏页表插入记录(页ID为P,recLSN为该条LSN)
分析阶段结束,事务表中包括了状态为U的事务集合。脏页表中混合油脏页和已经写入磁盘的页。
3.6.2 重做阶段
在重做阶段,重新执行所有事务的更新操作,包括已提交和其他状态的事务。
对于以下情况可以不重做:
- 受影响的页没有出现在脏页表
- 受影响的页出现在脏页表,但是脏页表中记录的该页的recLSN比当前log的LSN要大 (这里需要解析一下,比如log的LSN是100,但是该项的recLSN是200,recLSN指的是最早修改了某一页且还没有写入磁盘的log的LSN,证明LSN=100的log的更新已经被写入磁盘了,否则recLSN应该小于等于100)
- 把影响的页从磁盘中取出来(因为有IO所以放在最后检查),检查pageLSN,大于等于当前记录的LSN(大于不用检查的原因跟第二条一样,等于的原因是,已经写入磁盘了)
对于需要重做的log:重新执行操作;修改被修改页的pageLSN为log的LSN
3.6.3 反做阶段
通过分析阶段构造的事务表,取消崩溃时正在执行的事务的更新操作
这些事务被称为loser,将所有失败事务集合,称为ToUndo,反复从该集合中选择最大的LSN,处理的操作为:
- 记录为CLR,undoNextLSN不为空,将undoNextLSN加到ToUndo
- ... ... (略,过于繁琐不想看了)