数据库备份恢复及更新策略

首发于我的个人博客

几种常见的备份策略

周期性转储(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)序列. 之所以用链表, 是因为一个事务中可能会修改多个值.

// 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操作

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

    对redo也是同理

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

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

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

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

// 更新过程的伪代码
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记录去修改数据库.

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, 因为修改过程涉及两种日志规则.

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, 检查修改是否写入到数据库中. 等故障发生后直接从上一个检测点开始恢复.

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理