如果再有人问你数据库的原理,把这篇文章给他(18)

有多个原因让数据库不得不回滚事务:

  • 因为用户取消
  • 因为服务器或网络故障
  • 因为事务破坏了数据库完整性(比如一个列有唯一性约束而事务添加了重复值)
  • 因为死锁
有时候(比如网络出现故障),数据库可以恢复事务 。
这怎么可能呢?为了回答这个问题,我们需要了解日志里保存的信息 。
日志
事务的每一个操作(增/删/改)产生一条日志,由如下内容组成:
  • LSN:一个唯一的日志序列号(Log Sequence Number) 。LSN是按时间顺序分配的 *,这意味着如果操作 A 先于操作 B,log A 的 LSN 要比 log B 的 LSN 小 。
  • TransID:产生操作的事务ID 。
  • PageID:被修改的数据在磁盘上的位置 。磁盘数据的最小单位是页,所以数据的位置就是它所处页的位置 。
  • PrevLSN:同一个事务产生的上一条日志记录的链接 。
  • UNDO:取消本次操作的方法 。
    比如,如果操作是一次更新,UNDO将或者保存元素更新前的值/状态(物理UNDO),或者回到原来状态的反向操作(逻辑UNDO) ** 。
  • REDO:重复本次操作的方法 。同样的,有 2 种方法:或者保存操作后的元素值/状态,或者保存操作本身以便重复 。
  • …:(供您参考,一个 ARIES 日志还有 2 个字段:UndoNxtLSN 和 Type) 。
进一步说,磁盘上每个页(保存数据的,不是保存日志的)都记录着最后一个修改该数据操作的LSN 。
*LSN的分配其实更复杂,因为它关系到日志存储的方式 。但道理是相同的 。
** ARIES 只使用逻辑UNDO,因为处理物理UNDO太过混乱了 。
注:据我所知,只有 PostgreSQL 没有使用UNDO,而是用一个垃圾回收服务来删除旧版本的数据 。这个跟 PostgreSQL 对数据版本控制的实现有关 。
为了更好的说明这一点,这有一个简单的日志记录演示图,是由查询 “UPDATE FROM PERSON SET AGE = 18;” 产生的,我们假设这个查询是事务18执行的 。
如果再有人问你数据库的原理,把这篇文章给他

文章插图
 
每条日志都有一个唯一的LSN,链接在一起的日志属于同一个事务 。日志按照时间顺序链接(链接列表的最后一条日志是最后一个操作产生的) 。
日志缓冲区
为了防止写日志成为主要的瓶颈,数据库使用了日志缓冲区 。
如果再有人问你数据库的原理,把这篇文章给他

文章插图
 
当查询执行器要求做一次修改:
  • 1) 缓存管理器将修改存入自己的缓冲区;
  • 2) 日志管理器将相关的日志存入自己的缓冲区;
  • 3) 到了这一步,查询执行器认为操作完成了(因此可以请求做另一次修改);
  • 4) 接着(不久以后)日志管理器把日志写入事务日志,什么时候写日志由某算法来决定 。
  • 5) 接着(不久以后)缓存管理器把修改写入磁盘,什么时候写盘由某算法来决定 。
当事务提交,意味着事务每一个操作的 1 2 3 4 5 步骤都完成了 。写事务日志是很快的,因为它只是『在事务日志某处增加一条日志』;而数据写盘就更复杂了,因为要用『能够快速读取的方式写入数据』 。
STEAL 和 FORCE 策略
出于性能方面的原因,第 5 步有可能在提交之后完成,因为一旦发生崩溃,还有可能用REDO日志恢复事务 。这叫做 NO-FORCE策略 。
数据库可以选择FORCE策略(比如第 5 步在提交之前必须完成)来降低恢复时的负载 。
另一个问题是,要选择数据是一步步的写入(STEAL策略),还是缓冲管理器需要等待提交命令来一次性全部写入(NO-STEAL策略) 。选择STEAL还是NO-STEAL取决于你想要什么:快速写入但是从 UNDO 日志恢复缓慢,还是快速恢复 。
总结一下这些策略对恢复的影响:
  • STEAL/NO-FORCE 需要 UNDO 和 REDO: 性能高,但是日志和恢复过程更复杂 (比如 ARIES) 。多数数据库选择这个策略 。注:这是我从多个学术论文和教程里看到的,但并没有看到官方文档里显式说明这一点 。
  • STEAL/ FORCE 只需要 UNDO.
  • NO-STEAL/NO-FORCE 只需要 REDO.
  • NO-STEAL/FORCE 什么也不需要: 性能最差,而且需要巨大的内存 。
关于恢复
Ok,有了不错的日志,我们来用用它们!
假设新来的实习生让数据库崩溃了(首要规矩:永远是实习生的错 。),你重启了数据库,恢复过程开始了 。
ARIES从崩溃中恢复有三个阶段: