数据库数据存放的文件称为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。数据库的事务指令简单分为三类指令:
< 事务ID ,操作对象(operands), 旧值>
;对于redo log来说,这个记录< 事务ID ,操作对象, 新值>
整个流程就是先写日志再写数据,所以是write-ahead-logging
如果事务提交时返回个
在undo log看来,事务执行的步骤
在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刷入磁盘,很多英文资料都是这么说。
或者
的事务,进行回滚: < T i , a b o r t > commit
,后面再次宕机回滚就可以直接跳过了,否则每次回滚都要重复做。
如果在回滚期间再次宕机——这不是真正的问题
问题:如果一个事务多次对同一个对象修改怎么办?
其实这个问题在前面的 “从后向前扫描undo log”中已经解决了,其实理论上我们是要取日志中第一个旧值回滚就可以了,只不过实现的起来的时候稍微有点麻烦。从后往前扫描一来方便找到没有commit的事务,另外直接每次遇到一个就回滚,反正最后效果一样。
问题:如果有多个事务同时对一个对象修改怎么办?
假如,事务T1和事务T2都对A进行修改。假如T1先于T2,并且T1和T2都需要回滚。那么回滚的时候,如果先回滚T1,再回滚T2,对于T2来说,它的旧值就是T1的新值,因此回滚后最终值变成了T1修改后的值,导致数据库的原子性被破坏。 “从后向前扫描undo log”中已经解决了这个问题,我们先回滚T2,再回滚T1,T1的值覆盖T2回滚的值就好。
基本原理
commit
记录保障事务已经完全flush到磁盘,因此在回滚阶段不需要考虑
log 也是先写入内存再写入磁盘,这实现变得复杂了,因为log本身还有一致性问题
我们无法在每次操作时将日志刷新到磁盘上——这会导致IO过多
要避免的不良状态:
记录),但是新值不在磁盘里对于以上的难点,这篇文章里就不做讨论,因为我也没弄清楚哈哈,回头再写一个系列补充吧,慢慢来,这篇文章信息量已经很大了,我零零碎碎整理了好几天。
问题:从后往前进行扫描undo log,扫到啥时候是个头呢?
由于可能同时有多个事务进行写日志,所以我们并不是某个commit
前面的日志都是已经提交了的。例如事务T1先于事务T2,但是事务T2先提交,这时候宕机。那么T1是要回滚的,而T1的一些日志在
之前。因此,这种情况下,从后往前扫描并没有一个很好的结束条件,只能把整个log扫完,这做就很浪费时间。
我们可以用checkpoint来帮助提前结束undo 日志的扫描。checkpoint主要有两种模式,阻塞和非阻塞。
这是最简单的方式:
⟨ckpt⟩
(checkpoint 记录)到日志例子:
这时候我们undo,碰到
这个标志就可以结束了。
缺点:由于期间需要阻塞其它所有新来事务,这显然在大部分事务型数据库中无法让人接受
这个是非阻塞型的checkpoint,原理稍微复杂点。
算法:
,其中T1到Tk是指还没有提交的事务,也成为active
的事务
基本原理
标志之前开始的事务肯定都是已经提交了的事务,不用管,这里的前提是有对应的
到
之间可能有新事务,因为它是不阻塞的,这些事务可能在
之后才提交
标志结束例子
如上面的例子,T3事务在
之后开始的,并且提交前就宕机了,因此需要回滚。
问题:如果没有对应的
该怎么办?也就是checkpoint过程中就宕机了
简单分析一下,可以发现,如果没有
,那么
标志之前的开始的事务例如T1和T2可能没有提交,因此也需要回滚,并且因为部分日志在
之前,不能扫描到这里就结束了。因此,可以扫描到上一个
的地方。
例子
优缺点:
之前的日志下篇讲讲 redo log 以及它与checkpoint如何结合工作