数据库是所有信息系统的核心,但无论硬件多么可靠,故障总是难以完全避免——断电、操作系统崩溃、磁盘损坏、程序异常,任何一环出错都可能让正在运行的事务功亏一篑,甚至让已提交的数据凭空消失。一个合格的数据库管理系统,必须在故障发生后,有能力把数据库从"错误状态"拉回到"一致状态",而支撑这种能力的技术,就是数据库故障恢复。在软考数据库系统工程师的上午题中,故障恢复、日志文件、检查点、REDO与UNDO几乎年年出现,是分值不高却极容易丢分的考点。这篇文章把这一整套机制从概念到算法、从原理到命题陷阱一次性讲透。
先给数据库恢复下一个教材级的定义:数据库恢复,是指数据库管理系统在数据库发生故障后,利用存储在系统内部的冗余信息,把数据库从错误状态恢复到某个已知的一致状态的技术与过程。这里的"一致状态"是理解一切的起点——它指的是数据库中的数据满足完整性约束、事务的原子性和持久性得到保证的状态。恢复的目标不是把数据库恢复到"故障前那一瞬间"这么笼统的说法,而是恢复到"最近一个正确的一致状态"。
要理解恢复,必须先理解事务。事务是数据库操作的基本逻辑单位,具有原子性、一致性、隔离性、持久性四大特性。其中与恢复关系最密切的是原子性和持久性。原子性要求事务要么全部执行、要么全部不执行,一个事务中途失败时,它对数据库已经产生的部分修改必须被撤销,这就是UNDO(撤销)要做的事;持久性要求事务一旦提交,它对数据库的修改就必须永久保留,即使系统随后发生故障也不能丢失,这就是REDO(重做)要做的事。因此,恢复机制的本质,可以概括为一句话:用UNDO保证原子性,用REDO保证持久性。
为了把恢复机制讲清楚,还需要了解事务在执行过程中经历的状态转换。一个事务从启动到结束,通常依次经历以下状态:活动状态,指事务正在正常执行数据库读写操作;部分提交状态,指事务的最后一条语句已经执行完毕,但其修改结果尚未完全落盘;提交状态,指事务的所有修改都已永久写入磁盘;失败状态,指事务因故无法继续正常执行;中止状态,指事务被撤销、数据库回到事务开始前的状态。恢复机制关注的正是从部分提交到提交、以及从失败到中止这两条转换路径,前者需要REDO来保障,后者需要UNDO来完成。
数据库能够实现恢复,靠的是冗余。所谓冗余,就是数据库系统除了保存数据本身之外,还额外保存了关于数据的副本或记录,这些冗余信息在正常运行时看似是"浪费",在故障发生时却成为恢复的依据。数据库系统用于恢复的两大类冗余,一类是日志文件,记录每一次对数据库的更新操作;另一类是数据库的备份副本或镜像,是数据库在某个时刻的完整快照。理解了"事务""一致性""冗余"这三个概念,就抓住了恢复技术的全部出发点。
日志文件与备份副本各有分工。日志文件记录的是"变化过程",它保存的是数据库更新操作的流水账,粒度细到每一次插入、删除、修改,因此能够支持事务级、语句级的精确恢复;备份副本记录的是"某一时刻的静止状态",它保存的是整个数据库在某个时间点的完整拷贝,粒度粗,只能支持把数据库整体回退到备份时刻。两者的关系是:日志负责"精确回放和撤销",备份负责"大跨度回退",介质故障时往往需要两者配合,先靠备份恢复大体状态,再靠日志恢复到故障前的精确状态。
从数据库系统结构上看,恢复子系统与并发控制子系统、完整性控制子系统一样,都是数据库管理系统内核的重要组成部分。恢复子系统通过一个叫"恢复管理器"的模块统一调度日志的写入、检查点的建立、故障后的恢复执行。值得注意的是,恢复机制与并发控制机制是深度耦合的:并发控制通过封锁保证事务调度的可串行化,而恢复机制则保证在故障发生后,那些已经提交的事务的结果不被丢失、那些未提交的事务的结果被彻底清除,两者一前一后、一静一动,共同维护数据库的一致性。
日志文件是恢复机制的基石,理解日志就等于理解恢复的一半。日志由一条条"日志记录"顺序构成,每一条日志记录通常包含这样几个关键字段:事务标识符,用来区分这条更新是哪个事务做的;操作类型,标明是插入、删除还是修改;操作对象,即被更新数据的标识;更新前的旧值和更新后的新值。为什么既要记旧值又要记新值?因为UNDO需要旧值把数据改回去,REDO需要新值把数据改回来,两者缺一不可,这也是软考反复考查"日志文件保存了更新前和更新后的数据"这一结论的根源。
日志的写入有一个极其关键的顺序要求,这就是日志先行协议,英文缩写WAL。它的核心规则是:在对数据库中的数据进行修改之前,必须先把对应的日志记录写入稳定的日志文件中。换句话说,日志永远先于数据落盘,先写日志、后写数据库。为什么必须这样?设想如果先修改数据库、后写日志,那么在数据已经改完而日志尚未写入的一瞬间系统崩溃,这个修改就成了"无日志的修改",恢复时无从得知它是否属于一个已提交事务,也无法撤销或重做,数据一致性就永久破坏了。反过来,如果先写日志、后改数据,最坏的情况是日志写了而数据没改,恢复时只需依据日志补做即可,状态永远可追溯。WAL协议是数据库恢复的"铁律",也是事务持久性的最终保障。
还需要说明的是,日志记录并非一产生就立即写入磁盘日志文件,而是先写入内存中的日志缓冲区,再由系统按一定策略批量写回磁盘。之所以引入日志缓冲区,是因为逐条写磁盘的代价太高,批量写回能显著提升吞吐,这就是常说的"组提交"思想。但批量写回也带来了新的风险:日志缓冲区里的记录在系统故障时会随内存一起丢失,因此日志先行协议要求,事务在提交之前必须把该事务的所有日志记录从缓冲区强制刷新到磁盘日志文件,只有日志落盘之后,事务才算真正提交。
如果只有日志而没有检查点,那么故障发生后,恢复子系统必须从日志的最开头一直扫描到最后,去判断哪些事务需要重做、哪些需要撤销。这会导致两个问题:一是扫描的日志量巨大,恢复时间漫长;二是对已经提交很久的事务,重做一遍完全是浪费。检查点机制正是为了解决这个问题而设计的。
所谓检查点,就是数据库系统在某个时刻执行的一项操作:把内存缓冲区中所有已提交事务的修改强制写回磁盘数据库,并在日志文件中写入一条特殊的"检查点记录"。检查点记录中登记了此刻所有仍处于活动状态(即尚未提交)的事务清单。有了检查点,恢复时就无需从头扫描日志,只需从最近一个检查点开始处理,因为检查点之前的已提交修改都已经落盘,无需重做,只有检查点之后的部分需要重新判断。检查点建立的频率越高,故障后需要处理的日志越少,但建立检查点本身也要消耗系统资源,因此需要在恢复效率与运行开销之间权衡。软考常考的"模糊检查点"概念,指的是建立检查点时并不要求暂停所有事务,而是允许事务继续执行,这样既能限制恢复的日志量,又避免检查点成为系统吞吐的瓶颈。
当故障发生时,恢复管理器依据日志执行两类基本操作。UNDO操作针对"未完成的事务",即那些已经开始但尚未提交的事务。这些事务的部分修改可能已经写入磁盘数据库,为了保证原子性,必须依据日志中的旧值,把这些事务的修改逐一撤销,使数据库恢复到这些事务开始前的状态。REDO操作针对"已完成但可能未落盘的事务",即那些已经提交、但其修改可能还停留在内存缓冲区、尚未写入磁盘的事务。为了保证持久性,必须依据日志中的新值,把这些事务的修改逐一重做,确保已提交的结果不会因内存丢失而消失。
恢复算法的整体流程可以概括为两步走:第一步,从检查点记录出发,确定哪些事务在检查点时处于活动状态、哪些在检查点之后才启动、哪些已经结束;第二步,对未提交的活动事务执行UNDO,对已提交但结果可能未落盘的事务执行REDO。具体的判定策略因数据库实现而异,但万变不离其宗,本质都是"重做已提交的、撤销未提交的"。软考选择题中反复出现"系统故障恢复需要UNDO和REDO"这一结论,正是因为系统故障会同时造成内存中未提交事务的丢失和已提交事务的未落盘,两类问题并存,两类操作缺一不可。
数据库故障按其影响范围和数据破坏程度,教材中分为三类。第一类是事务故障,指单个事务在运行过程中因自身原因而异常终止,比如运算溢出、违反完整性约束被系统拒绝、发生死锁而被撤销等。事务故障只影响该事务本身,不影响其他事务,也不破坏磁盘上的数据。这类故障的恢复最简单:只需对该事务执行UNDO,把它已经产生的修改全部撤销即可,而且由于日志中记录了旧值,不需要借助备份。
第二类是系统故障,也叫软故障,指造成系统停止运转、内存内容丢失但磁盘数据不被破坏的故障,最典型的就是断电、操作系统崩溃、数据库系统进程异常终止。系统故障发生时,内存缓冲区中尚未写入磁盘的数据全部丢失,此时磁盘上可能有两种"不一致":一种是已经
本篇完!