数据库事务故障恢复

事务管理

事务的定义

事务是一系列的数据库操作,作为一个不可分割的执行单元(unit),要么全做要么全不做。

在这一部分单纯讨论事务的并发时,我们通常定义一个简单的事务模型,假设事务的写操作是直接写到磁盘(实际上会有页面的缓冲池)的原子操作(因此另一个并发事务可以读取到最新数据)。而事务并非总能成功执行,但事务出错终止(aborted)时,为了保证原子性,事务终止前对数据库做的操作都需要撤销,需要对事务进行回滚(rolled back),数据库系统通常是基于日志(log)进行故障恢复。

事务的四个特性(ACID):

  • 原子性(Atomicity): 事务是最小的执行单位,要么全做,要么全不做。
  • 一致性(Consistency): 执行事务前后,数据都是处于一致性状态,即该状态满足预定的约束(比如转账模型中,事务执行前后,两个账户余额总数应该是不变的)。
  • 隔离性(Isolation): 多事务并发执行时,各个并发事务之间都是独立的,感觉不到其他事务的存在。
  • 持久性(Durability): 一个事务成功完成后,对数据库的改变是持久的,即使发生系统故障。

故障恢复

故障分类

  • 事务故障: 事务逻辑错误或者并发事务出现死锁。
  • 系统故障: 硬件或者软件的错误或漏洞导致数据库系统发生故障。导致内存数据丢失(已提交事务可能数据还在内存没刷回磁盘,需要重做,未提交事务)。
  • 磁盘故障: 非执行时错误,而是写入磁盘时因为磁盘故障导致磁盘数据丢失。

日志记录

日志是包括一系列的日志记录(log record),基本的日志记录类型包括:

  • 更新日志记录,<T,X,V1,V2>,其中T表示事务标识,X是数据项标识,V1是旧值,V2是新值。
  • 事务开始记录,<T,start>
  • 事务提交记录,<T,commit>
  • 事务终止记录,<T,abort>

每次事务执行写操作,需要先写日志,再执行操作。

Redo和Undo

发生系统故障时,可以利用日志对事务进行重做(Redo)或者撤销(Undo)。

  • 如果一个事务包括start,但不包括commit和abort,说明这个事务在系统故障前开始,但是没有完成或回滚,因此此时数据更新有可能在内存,也可能已经刷盘,为了保证数据的一致性,必须进行Undo操作。

  • 如果一个事务包括start,以及包括commit或者abort,说明这个事务的所有操作都已经完成,都记录在日志里,但是也不知道修改是否已经刷盘,所以必须根据日志记录,进行Redo操作。(已经commit或者abort的事务必须Redo而不是Undo,因为修改有可能已经写入磁盘?)

  • Undo操作除了把数据恢复成旧值,还会把恢复操作作为一个更新操作写入为一种只读的特殊日志,不需要记录恢复操作的旧值和新值。最后写入一个abort日志,表示事务撤销完成。

  • Redo操作按顺序将数据设置成新值。

检查点(checkpoint)

系统会定时将缓存中的脏页刷回磁盘,并用另外的日志记录下当前记录点的时刻,系统故障恢复时不需要考虑最后一个记录点之前成功提交的事务。

事务回滚

正常的事务回滚,从后往前扫描日志,对于该事务的每一个日志记录<T,X,V1,V2>,将数据项X写入旧值V1,然后往日志末尾添加一个特殊的只读日志记录<T,X,V2>,注意这种特殊记录不需要旧值,不会被Undo。

当遇到该事务的<T,start>记录,停止扫描,并往日志末尾写入<T,abort>表示回滚完成。

系统故障的恢复

利用日志记录进行系统故障的恢复(Recovery after a system crash),书上所描述的完整算法如下:

  • 重做(redo)阶段:把该redo的事务进行redo,并构造该undo的undo-list(包括系统奔溃前未完成的事务,即没有提交也没有回滚)。

    • 初始化undo-list为最后一个检查点(checkpoint)的活跃事务(+检查点之前开始的事务)。
    • 从最后一个检查点开始正向扫描日志记录,遇到一个普通日志记录<T,X,V1,V2>或者read-only特殊日志记录<T,X,V2>(表示重做回滚(比如自定义的某些条件下的事务回滚而不是意外的事务回滚)),执行redo操作,将数据项X写入V2值。
    • 遇到<T,start>,将该事务加入undo-list(+检查点之后开始的事务)。
    • 遇到<T,commit>或者T<T,abort>,将该事务从undo-list中删除(-故障发生点之前提交或回滚的事务(第2步的redo操作中就对这些事务进行了重做))。
  • 撤销(undo)阶段:

    • 反向扫描日志,遇到属于undo-list的事务日志,执行undo操作,将数据项写回旧值。
    • 遇到属于undo-list的事务的<T,start>,把该事务从undo-list删除,写入一个<T,abort>日志记录(该事务已撤销完成)。
    • 当undo-list为空,则所有事务撤销完成。

为了方便处理,undo-list的事务在重做阶段也会被重做,然后在撤销阶段再被撤销。

MySQL中的事务恢复

PostgreSQL中的事务恢复