几种常见的备份策略

周期性转储(Periodical dumping)

周期性转储

每隔一段时间将数据库的内容进行备份, 当数据库在两次备份之间发生故障时,可以将数据库恢复至最近一次备份的状态. 但是最近一次备份到故障点之间的数据全部丢失dumpingfailure之间.

备份与增量转储(Backup and Incremental dumping)

备份与增量转储

两次备份之间的间隔时间不能太短, 但可以在备份之间进行增量转储(I.D), 在较短的时间间隔内将数据库变化的部分备份下来. 当故障发生时, 将数据库恢复至最近的一次备份, 再累加上I.D, 这样做仍然存在丢失更新的问题, 比周期性转储要好些. 问题的性质没有变.

备份与日志(Backup and Log)

备份与日志

从上次备份以来, 将数据库的所有变化以日志的形式记录下来. 日志的内容如下:
日志的内容

日志要记录两个部分, 修改(change)的旧值, 简称前项 B.I和新值, 简称后项A.I, A.IB.I都要记录到日志里. 具体到每一种操作(插删改)该如何记录呢?

op B.I A.I
update B.I A.I
insert - A.I
delete B.I -

只有update操作才需要记录两个值.
当数据库发生故障时, 将数据库恢复到最近的一次备份, 然后将日志信息进行重新加载. 这种恢复的好处就是不会丢失更新.
当数据库进行恢复时, 一些事务可能被中断(half down), 这时就要采用B.I的值进行回滚(undo). 一些事务也可能完成了, 但是结果还没有及时写回到数据库中, 这时就要用日志中的A.I进行重写操作(redo).

数据库的恢复策略

事务的概念

事务(Transaction)T是数据库操作的最基本单位, 是一串操作的集合, 当我们在客户端输入SQL语句时, 会自动将其包装为一个事务. 它具有以下的性质(ACID):

  1. 原子性(Atomic): 要么成功, 要么失败 Nothing or All

  2. 一致性(Consistency): 一个事务运行后, 数据库从一个一致性状态到达另一个一致性状态

  3. 隔离性(Isolation): 并发事务不能相互干扰, 好像它独占整个数据库

  4. 持久性(Durability): 一个事务成功完成, 它对数据库的影响就应该是永久的, 哪怕出现故障也是可恢复的

与恢复有关的数据结构

日志应该存储在非挥发存储器中(novolatile). 事务运行时, BEGIN TRANSCATION语句运行, 数据库要为每一个事务分配一个标识TID. 数据库还要维护两张表, 提交事务列表(Commit list, C.L)与活动事务列表(Active list, A.L). C.L中维护着已经完成且提交的事务, A.L维护着正在运行的事务. 日志的具体结构如下:

日志的具体结构

一条日志信息由TID标识, 其中的两个链表, 指向一个含有记录所在物理块地址与A.IB.I的结构(change)序列. 之所以用链表, 是因为一个事务中可能会修改多个值.

1
2
3
4
5
// change可能的结构
struct change {
block_addr BID;
value v;
}

事务执行时日志遵循的规则

  1. 提交规则(Commit Rule)

    事务提交之前, A.I必须写入到非挥发存储中. 并非一定是数据库, 也可能是Log

  2. 日志优先规则(Log Ahead Rule)

    如果A.I在事务提交之前直接写入到数据库中, 那么B.I就必须首先(first)写入到Log中

  3. 恢复策略(Recover Strategies)

    reduundo的幂等性
    对一个对象的B.I进行多次undo操作等价于作一次undo操作

    1
    undo(undo(undo...(x))) = undo(x)

    对redo也是同理

    1
    redo(redo(redo...(x))) = redo(x)

数据库的更新策略以及故障恢复

直接修改数据库(A.I->DB before commit)

对数据库的修改都要将记录的B.I先写入到Log中, 然后将A.I写入到数据库中. 当事务进入提交阶段, 将事务加入到提交列表中, 同时从活动列表中删除.

1
2
3
4
5
6
7
8
9
10
11
// 更新过程的伪代码
TID->A.L
loop {
B.I->Log
A.I->DB
} until ( no more options )

commit (
TID->C.L
delete TID from A.L
)

这种更新策略的故障处理, 要根据两张事务表的状态决定. 根据TID去检查两张表, 检查事务的状态.

in C.L? in A.L? op
true tue delete TID from C.L
true - do nothing
- true undo & delete TID from A.L

如表, 当进行恢复时, 如果发现TID在A.L中而不在C.L中, 那么事务没有完全结束, 就要对数据进行回滚.

事务提交后修改(A.I->DB after commit)

对数据库的更新有可能会失败, 这种策略首先将对数据库的更新记录到Log中, 等事务进入到commit阶段后, 再根据Log记录去修改数据库.

1
2
3
4
5
6
7
8
9
10
11
12
TID->A.L
loop {
A.I->Log
} until ( no more options )

coomit {
TID->C.L
loop {
A.I->DB
} until ( no more log )
delete TID from A.L
}

恢复策略与上种方法十分类似

in C.L? in A.L? op
true tue redo, delete TID from A.L
true - do nothing
- true delete TID from A.L

在上表中, 如果一个事务没有完成, 直接将其从C.L删除, 因为数据修改是在事务提交后进行的. 第一种情况下, 数据还没有完全从Log中写入到数据库, 由于redo的幂等性, 将数据全部redo即可, 不用理会故障前在哪个阶段. 这种策略下, 并发事务的效率更高.

并发提交(A.I->DB concurrently with commit)

安排一个线程, 在硬盘空闲周期并发地将Log中的B.I写入到数据库中. A.IB.I要同时写入Log, 因为修改过程涉及两种日志规则.

1
2
3
4
5
6
7
8
TID->A.L
A.I, B.I->Log
A.I->DB (concurrently)
...
commit {
TID->C.L
A.I->DB
}

恢复:

in C.L? in A.L? op
true tue redo, delete TID from A.L
true - do nothing
- true undo, delete TID from A.L

监测点(check poit)

事务表不可能一直增长下去, 在数据库中, 每隔一段时间就检查一遍Log, 检查修改是否写入到数据库中. 等故障发生后直接从上一个检测点开始恢复.