数据库undo和redo日志(一)

数据库数据存放的文件称为data file;日志文件称为log file;数据库数据是有缓存的,如果没有缓存,每次都写或者读物理disk,那性能就太低下了。数据库数据的缓存称为data buffer,日志(redo)缓存称为log buffer;既然数据库数据有缓存,就很难保证缓存数据(脏数据)与磁盘数据的一致性。在任何地方,只要考虑到读写缓存,就得考虑一致性的问题了。

当数据库中的查询更新一个字段时,并不是将修改刷入磁盘才返回结果给用户,而是将log刷入磁盘就给用户结果了,后面由系统将修改按page的粒度刷入磁盘,这样能显著提高IO效率与查询性能。如果返回给用户结果后,还没刷入磁盘(或者部分已经刷入磁盘),数据库就宕机了,数据库如何保证用户得到的结果与数据data_file一致呢?这里就是用事务的ACID保证的。

数据库中,使用write-ahead-logging(WAL) 技术来保障事务的持久性和原子性。write-ahead意思是说,事务中的每个操作在刷入磁盘前,都必须先把对应的操作日志刷入磁盘。

主要有两种write-ahead logging:undo log 和 redo log。数据库的事务指令简单分为三类指令:

  1. log op:log 事务的一个op 。对应undo log来说,这个记录包含< 事务ID ,操作对象(operands), 旧值>;对于redo log来说,这个记录< 事务ID ,操作对象, 新值>
  2. flush op:将事务的op刷入磁盘
  3. log commit:log一个commit

整个流程就是先写日志再写数据,所以是write-ahead-logging

undo log 工作原理

如果事务提交时返回个

undo log 记录步骤

在undo log看来,事务执行的步骤

  1. log ops;
  2. flush ops;
  3. log commit

在undo log 看来,一定是先把事务之前的旧值先log下来,再去把这个op刷入磁盘。一个事务可能有多次刷磁盘操作,如果在提交前数据库宕机,那么可能部分op已经刷入磁盘了。这样的事务就需要回滚,将对磁盘中的修改撤回,因此就要用旧值来覆盖刷入磁盘中的数据。

举个例子

Transaction T1 Log Comment
⟨T1,start⟩ when the transaction starts
read(A,t); t←t×2;
write(A,t) ⟨T1,A,8⟩ now it’s allowed to output(A)
read(B,t); t←t×2;
write(B,t) ⟨T1,B,8⟩ now it’s allowed to output(B)
output(A)
output(B) now all modifications are on disk
⟨T1,commit⟩ transaction has finished

上面的例子中,write(A, t)指的是把内存中A的值改成t;output(A)指把A刷入磁盘,很多英文资料都是这么说。

回滚事务的步骤

  • 从后向前扫描undo log,找到没有或者 的事务,进行回滚:
  • 对于所有的 ⟨ T i , X , v ⟩ ⟨T_i,X,v⟩ Ti,X,v
    • write(X,v)
    • output(X)
  • < T i , a b o r t > <Ti,abort>写入undo log

< T i , a b o r t > <Ti,abort> 记录已经abort的事务的commit,后面再次宕机回滚就可以直接跳过了,否则每次回滚都要重复做。

如果在回滚期间再次宕机——这不是真正的问题

  • 我们将再次覆盖旧值
  • 两次写入旧值与一次写入相同(幂等性)
  • 这样可以保证回到一致状态

问题:如果一个事务多次对同一个对象修改怎么办?

其实这个问题在前面的 “从后向前扫描undo log”中已经解决了,其实理论上我们是要取日志中第一个旧值回滚就可以了,只不过实现的起来的时候稍微有点麻烦。从后往前扫描一来方便找到没有commit的事务,另外直接每次遇到一个就回滚,反正最后效果一样。

问题:如果有多个事务同时对一个对象修改怎么办?

假如,事务T1和事务T2都对A进行修改。假如T1先于T2,并且T1和T2都需要回滚。那么回滚的时候,如果先回滚T1,再回滚T2,对于T2来说,它的旧值就是T1的新值,因此回滚后最终值变成了T1修改后的值,导致数据库的原子性被破坏。 “从后向前扫描undo log”中已经解决了这个问题,我们先回滚T2,再回滚T1,T1的值覆盖T2回滚的值就好。

基本原理

commit记录保障事务已经完全flush到磁盘,因此在回滚阶段不需要考虑

  1. 如果事务已经提交了,那么就不用回滚了,因为所有操作都在commit前都flush 到磁盘了
  2. 如果事务没有提交,对于那些已经刷入磁盘的ops,肯定都有对应的log,根据log 的旧值恢复。

存在挑战

  • log 也是先写入内存再写入磁盘,这实现变得复杂了,因为log本身还有一致性问题

  • 我们无法在每次操作时将日志刷新到磁盘上——这会导致IO过多

要避免的不良状态:

  • 数据库操作已经刷入磁盘,但尚未写入相应的日志记录(日志还在内存没刷入磁盘)
  • 整个日志都在磁盘上(包括记录),但是新值不在磁盘里

对于以上的难点,这篇文章里就不做讨论,因为我也没弄清楚哈哈,回头再写一个系列补充吧,慢慢来,这篇文章信息量已经很大了,我零零碎碎整理了好几天。

Checkpoint

问题:从后往前进行扫描undo log,扫到啥时候是个头呢?

由于可能同时有多个事务进行写日志,所以我们并不是某个commit前面的日志都是已经提交了的。例如事务T1先于事务T2,但是事务T2先提交,这时候宕机。那么T1是要回滚的,而T1的一些日志在之前。因此,这种情况下,从后往前扫描并没有一个很好的结束条件,只能把整个log扫完,这做就很浪费时间。

我们可以用checkpoint来帮助提前结束undo 日志的扫描。checkpoint主要有两种模式,阻塞和非阻塞。

stop the world

这是最简单的方式:

  • 不接受任何新事务 (say “stop” to everybody)
  • 等待当前所有正在运行的事务完成
  • 将它们所有的修改刷入磁盘 (包括commit 日志)
  • 写入⟨ckpt⟩(checkpoint 记录)到日志
  • 恢复接受事务

例子:

数据库undo和redo日志(一)_第1张图片

这时候我们undo,碰到这个标志就可以结束了。

缺点:由于期间需要阻塞其它所有新来事务,这显然在大部分事务型数据库中无法让人接受

Non-Quiescent Checkpoint

这个是非阻塞型的checkpoint,原理稍微复杂点。

算法:

  • 记录日志,其中T1到Tk是指还没有提交的事务,也成为active的事务
  • 等待事务T1到Tk全部提交或者abort,这期间它们的相应undo 日志也记录完
  • 当所有T1到Tk全部完成,记录日志

基本原理

  • 标志之前开始的事务肯定都是已经提交了的事务,不用管,这里的前提是有对应的
  • 之间可能有新事务,因为它是不阻塞的,这些事务可能在之后才提交
  • 因此利用undo log 进行回滚时,应该从后往前扫描,直到标志结束

例子

数据库undo和redo日志(一)_第2张图片

如上面的例子,T3事务在之后开始的,并且提交前就宕机了,因此需要回滚。

问题:如果没有对应的该怎么办?也就是checkpoint过程中就宕机了

简单分析一下,可以发现,如果没有,那么标志之前的开始的事务例如T1和T2可能没有提交,因此也需要回滚,并且因为部分日志在之前,不能扫描到这里就结束了。因此,可以扫描到上一个 的地方。

例子

数据库undo和redo日志(一)_第3张图片

优缺点:

  • (+)不用记录所有日志,可以删除上一个之前的日志
  • (-)不利于做数据库备份,因为undo log是反向进行redo的,因此必须等后面的日志写入才行。
    • 可以使用stop the world策略做备份
    • 可以用redo logging 技术来解决这个问题

下篇讲讲 redo log 以及它与checkpoint如何结合工作

参考

  1. https://blog.cykerway.com/posts/2018/11/18/database-undo-log-and-redo-log.html
  2. http://www.mathcs.emory.edu/~cheung/Courses/554/Syllabus/6-logging/undo-redo3.html
  3. http://mlwiki.org/index.php/Undo_Logging

你可能感兴趣的:(数据库)