事务的原子性和持久性
原子性和持久性密切相关,原子性保证了 多个数据库操作一起成功或者一起失败,持久性保证了在事务完成后写入的数据不会被撤销或丢失。
实现原子性和持久性最大的问题是“写入磁盘”这个操作是在一个时间段内完成的,它不是原子操作,存在着“未写入”、“正在写入”、“已写入”三种状态。接下来看一下如何处理好这个阶段的问题,来实现事务原子性。
Commit Logging
Commit Logging 实现事务的方式非常简单。开启事务后,执行的数据库操作不会直接写入磁盘,而是先写入日志中,当事务提交完成会在日志中写入“Commit Record”
,然后数据会根据日志记录的内容写入磁盘,在写入磁盘完成后,日志还会加入一条“End Record”
,这种方式就称为Commit Logging。
可能发生的故障
Commit Record之前发生故障:在提交事务前,写入日志时,如果系统发生故障,那么日志文件就不存在Commit Record记录,那么下次数据库恢复时发现日志中没有Commit Record记录就可以回滚之前的数据库操作日志,不进行后续的写入磁盘操作。
Commit Record之后,End Record之前发生故障:此时事务已经提交,日志文件中完整的记录了这次事务的数据库操作,开始写入磁盘。如果系统发生故障,那么日志文件就不存在End Record记录,那么下次数据库恢复时发现日志中没有End Record记录就可以通过日志恢复之前写入磁盘的数据。
缺点
Commit Logging 在大多数情况下已经可以实现事务的原子性和持久性,但是这种方式的“写入磁盘”操作必须在日志完整的记录了数据库操作之后,如果事务中数据库操作很多,那么就会占用大量的内存缓冲,直到日志上写入Commit Record
,才开始将数据写入磁盘。
可以发现这样的设计使得Commit Logging的整个过程是串行的,当事务较大时磁盘IO空闲时间很长。
FORCE
force策略要求事务提交后变动的数据马上写入磁盘,这种策略没有日志保护,如果在写入磁盘时数据库崩溃,那么就会产生数据的不一致,不能保证事务的原子性。
no-force策略加入了(Redo Log)日志保护,开启事务后先将变动的操作写入日志,后将数据写入磁盘,这种策略在数据库崩溃时可以根据日志进行恢复,但是必须等日志记录完,才能开始写入磁盘操作,这种串行的方式会影响数据库性能。
Redo Log,一般翻译为“重做日志”,Commit Logging就是no-force策略
STEAL
我们为什么不在开始写入日志时,同时将变动的数据写入磁盘,让写入磁盘操作”偷摸“地进行呢?
确实可以,STEAL策略加入了Undo Log,在变动数据写入磁盘前,Undo日志会记录数据变动的位置,和变化前后的数据,当事务回滚和奔溃恢复时通过Undo Log对这部分偷跑的数据进行恢复。
Undo Log 现在一般被翻译为“回滚日志”
Write-Ahead Logging
预写入日志使用了no-force + steal 它保证了数据崩溃可以正常恢复,同时可以让数据在书屋提交前就写入磁盘。
拥有了Redo Log和Undo Log,当系统发生崩溃时会有以下三个阶段
- 分析阶段(Analysis):该阶段从最后一次检查点(Checkpoint,可理解为在这个点之前所有应该持久化的变动都已安全落盘)开始扫描日志,找出所有没有 End Record 的事务,组成待恢复的事务集合(一般包括 Transaction Table 和 Dirty Page Table)。
- 重做阶段(Redo):该阶段依据分析阶段中,产生的待恢复的事务集合来重演历史(Repeat History),找出所有包含 Commit Record 的日志,将它们写入磁盘,写入完成后增加一条 End Record,然后移除出待恢复事务集合。
- 回滚阶段(Undo):该阶段处理经过分析、重做阶段后剩余的恢复事务集合,此时剩下的都是需要回滚的事务(被称为 Loser),根据 Undo Log 中的信息回滚这些事务。
预写入日志是最复杂也是性能最好的实现方式,可以通过下面这张图看一下Undo Log
、Redo Log
和force
、no-force
之间的关系
总结
Redo Log 通过Commit Record和End Record 保证了事务的原子性和持久性,Undo Log会记录提前写入磁盘的数据变化情况,当系统崩溃时通过Undo Log 来恢复变动数据。
Write-Ahead Logging 式的日志写入方法,通过分析、重做、回滚三个阶段实现了 STEAL、NO-FORCE,从而实现了既高效又严谨的日志记录与故障恢复。