《数据库实现》-- 第六章 故障恢复

第六章, 故障恢复

概述

故障恢复, 依赖很多组件一起协同完成, 

  1. 日志管理器, 存储日志
  2. 事物管理器, 保障事物的一致性
  3. 恢复管理器, 备份和恢复的协调者

image.png

事物原语操作

地址空间

  1. 物理磁盘块空间
  2. 缓冲区的内存地址
  3. 事物的局部地址空间

一种描述数据在地址空间移动的操作:

  1. input(x),  将磁盘块含有数据库元素 x  load 到内存,  由缓冲区管理器触发
  2. read(x), 将数据库元素x 拷贝到事物的局部变量t, (如果不在内存,则先执行input(x), 然后赋值给事物局部变量t)。 事物管理器触发
  3. write(x), 将事物局部变量写到内存x。 事物管理器触发。
  4. output(x), 刷到磁盘。 缓冲区或日志管理器触发。

 

因为事物的原子性, 有一些设计上的要求

  1. 数据库元素大小不超过一个块 (无论是在内存中操作或磁盘io操作)

例子:
需求
image.png
则事务的原语是
image.png
如果不再缓冲区, 还需要增加input 指令。 

  1. 在output 之前,发生故障, 都能保持数据库一致性
  2. 在output 之后, 发生故障,也能保持数据库一致性
  3. 在output过程中,发生故障, 会破坏一致性。

 

undo 日志

undo 日志, 撤销事务在系统崩溃前还没有完成的动作来修复数据库状态。 

日志通常是一个三元组<T, X, V>,  事务改变了数据库元素x, x原来的值是v。 

  1. 日志是反映write 动作,不反映output动作 
  2. 日志记录是原始值, 不记录新值。 
  3. 如果事务t 改变了数据库元素x, 日志必须在数据刷到磁盘前,日志写入磁盘
  4. 如果事务提交, commit日志必须在所有元素写到磁盘之后写到磁盘,而且要快。

 

新的逻辑
image.png

undo 日志恢复
恢复管理器, 

  1. 事务分已提交和未提交日志。 
    1. 恢复时, 按顺序从尾部向头部扫描
    2. 如果发现commit 日志, 则事务已经刷到磁盘,不需要额外恢复操作
    3. 当发现abort 日志记录或发现start,但没有commit日志时, 这个事务必须被回滚。 
      1. 用日志内部的原始值更新到磁盘, 强制刷到原始值
      2. 如果没有abort 日志, 写入abort 日志
  2. 恢复过程中的二次崩溃, undo的设计时幂等的,可重入的。

 

checkpoint
定期做checkpoint, 清理老的日志, 避免恢复到一个比较过时的状态。
静态checkpoint

  1. 停止新事务
  2. 等待当前活跃的事务, 提交commit或abort
  3. 刷日志到磁盘
  4. 写入日志记录ckpt
  5. 重新接收事务

动态checkpoint
静态checkpoint 需要停止新事务, 这个过程对用户非常不友好。

  1. 写入 日志记录< start ckpt(t1, t2, t3, …. tk)> 其中t是所有活跃的事务
  2. 等待t1, t2, t3, 。。。 tk 提交或中止, 但允许其他事务进行
  3. 当t1, t2, t3, 。。。。 tk 完成后, 写入

当写入end ckpt 后,可以将上一个start ckpt 之前的记录全部清楚掉

lsn
日志经常是使用旧的日志文件, 会循环使用旧的数据库, 这种情况下, 日志得有一个只增不减的序列号。 
image.png
如果发现下一条记录的lsn 比当前记录的lsn还要小, 说明已经到结尾了。 

日志通常是一个多层次结构, 顶层文件记录日志文件的构成, 顶层文件的最后一条, 表明可以在什么地方找到最后一条记录。 

先不考虑循环文件, 如何恢复, 有了checkpoint, 恢复的速度可以大大加快, 恢复可以提交停止。 

  1. 还是按照之前的逻辑进行恢复 
  2. 如果先遇到, 向前扫描, 当遇到start ckpt 就停止, start ckpt 之前的日志没有必要再恢复了
  3. 如果先遇到start ckpt, 记录这个ckpoint 所涉及的事务列表, 向前扫描, 找到最早出现这些事务的日志的地方, 之前的事务日志就不用再扫描了。

 

redo 日志

redo 日志忽略未完成的事务, 重复已经提交事务所做的改变。 

redo 日志是记录commit后的值
<t, x, v>, 事务t 为数据库元素x 写入新值v

image.png

整个顺序是不同

  1. 写到内存中redo日志
  2. 写commit日志
  3. 在output磁盘前,刷redo log

redo log 的恢复
从日志首部向下扫描

  1. 如果t是未提交的事务, do nothing
  2. 如果t是已经提交的事务, 写入v
  3. 对没有完成的事务, 写入一个到日志

redo 的checkpoint

在真实环境中, 数据output 到磁盘会比较慢, 但redo 是按时刷到磁盘。 
写checkpoint过程
image.png

带检查点redo恢复

  1. 只找最后一个start ckpt提到的事务即可
  2. 如果是end ckpt,则记录这个ckpt 对应的事务列表,  对应start ckpt 前提交的事务已经刷到磁盘, 不需要恢复
  3. 对ckpt 列表的事务 进行恢复

undo/redo 一体日志

日志格式<T, X, v, w>, v 为修改前的值, 新值为w
image.png

MySQL innodb undo 和redo 协同工作

假设有A、B两个数据,值分别为1,2.
A.事务开始.
B.记录A=1到undo log.
C.修改A=3.
D.记录A=3到redo log.
E.记录B=2到undo log.
F.修改B=4.
G.记录B=4到redo log.
H.将redo log写入磁盘。

I.事务提交

innodb 有2个优化

  1. redo log 不是直接写入磁盘, 是先写入redo log buffer, 然后定期flush到磁盘, 因此,当flush到磁盘时, 会把一批commit和abort/uncommitted的redo log 都刷到了磁盘, 后续如何处理依赖undo log
    1. 在恢复的时候, 是结合undo log 一起进行恢复, 因为在恢复的过程中,还有一些log是没有commit的,但已经刷到了磁盘, 当前还是会把他apply,但通过undo log来进行恢复

     

  2. redo log的文件空间是预分配的,为了最优性能,操作是append only
  3. 把undo log当作数据来处理, 因此undo log 也能用buffer来缓存, undo log 也是定期刷磁盘, 而不是每次commit必须刷磁盘

记录1:<trx1,Undo Loginsert<undo_insert…>>
记录2:<trx1,insertA…>
记录3:<trx1,Undo Loginsert<undo_update…>>
记录4:<trx1,updateB…>
记录5:<trx1,Undo Loginsert<undo_delete…>>
记录6:<trx1,deleteC…>
记录7:<trx1,insertC>
记录8:<trx1,updateBtooldvalue>
记录9:<trx1,deleteA>

针对介质的防护

备份

  1. 备份可以使用日志
  2. 备份分2个等级
    1. 全量转储
    2. 增量转储, 第一次为0级, 而后每一级即为上一次的delta备份

日志方式倾向于redo或者undo/redo 一体 日志, 使用undo 日志干不了

  1. 写入
  2. 根据采用的日志方式(如redo log)执行一个适当的检查点
  3. 执行转储, 拷贝数据到远程节点
  4. 至少dump 第二步中的checkpoint 和checkpoint之前的日志完成dump
  5. 写入日志记录

恢复

  1. 先找全量转储
  2. 再找增量转储,