事务日志
转自:http://nosql-wiki.org/foswiki/bin/view/Main/TransactonLog
日志保证了数据的持久性和事务的原子性。可以简单的认为日志是一个不断追加日志记录的文件。单条日志记录是一段二进制缓冲区。 下面是本文会使用到的几条通用的日志记录:
- 标示trasaction的开始
- 标示transcatoin成功提交,所有对数据的修改都已经成功。由于cache的存在,在日志中看到COMMIT并不一定意味着 数据的修改都已经持久化。日志的目的就是保证所有COMMIT的事务的修改在程序程序异常退出的情况下能够保留;所有没有COMMIT 的事务的修改在程序异常退出的情况下都不会保留,就像这些事务根本就没有START一样。
- 事务T的所有修改都不能保留下来,最终体现出来的东西就像事务T根本就没有START过。
日志的类型主要有:undo log,redo log,undo/redo log。最经常使用的类型为redo log和undo/redo log。 如果系统不出现异常情况, 那么日志是没有必要的,因此在讲述的时候大部分场景都是指系统异常退出重启,在叙述过程中不在重复描述这个场景前提。
UNDO LOG
日志内容
undo log就是把所有没有COMMIT的事务回滚到事务开始之前的状态,对于已经commit的事务不做任何处理。 因为对于commit的事务不做任何 处理,那么在写COMMIT日志记录之前,事务对数据的修改都必须已经持久化, 如果只有一部分持久化,事务修改的数据会处于不一致状态。 另外,由于需要做undo操作,因此日志记录中必须包含数据修改前的值, 单条的undo log形式为,表示在事务T开始运行前, X 的值为v;由于对未commit的事务必须进行undo操作,那么在对数据库的数据进行修改之前,必须先保证事务日志已经持久化, 如果日志没有持久化,并且最后事务没有commit,那么数据就无法回滚到事务开始前的状态。由此可以总结出undo log必须满足的两条规则:
- U1: If transaction T modifies database element X, then the log record of the form must be written to disk before the new value of X is written to disk.
- U2: If a transaction commits, then its COMMIT log record must be witten to disk only after all database elements changed by the transaction have been written to disk, but as soon thereafter as possible
因此,对于日志和数据本身持久化的顺序为:
- 事务会修改的所有数据,反应对数据进行修改的日志记录
- 数据本身
- COMMIT日志
在这里我们可以看到,每个事务commit之前必须把对数据的修改进行持久化,这样会导致性能问题, 因为每次事务都回带来一次数据文件的 sync 写入,而使用日志的主要目的就是减少磁盘的sync操作。 因此undo log存在较大的性能问题,因此在实际中使用并不太多。
日志回放
系统重启回放日志,只需要从后往前扫描日志文件,对于所有没有commit的事务按照日志记录中的数据做回滚操作。
CheckPoint
按照上述方法回放日志,需要扫描所有日志文件,并且日志文件不能删除。但是实践上我们可以看到, 如果一个事务已经commit了那么之前的 日志记录其实就可以不回放了。因此引入checkpoint的概念。
最简单的checkpoint做法是在做checkpoint的时候阻塞所有更新,直到所有未决的事务都commit活着abort, 然后记录。在日志回放的时候, 如果碰到就停止回放日志。阻塞更新当然不是优雅的处理方式。 下面介绍一种不阻塞更新的checkpoint方法。
生成checkpoint的过程为:
- 记录START_CKPT<T1,T2,...,Tn>,其中Ti表示开始生成checkpoint的时候未决的事务(没有提交的事务)
- 等待所有事务提交
- 记录END_CKPT
日志回放的时候,如果首先遇到END_CKPT,只需要回放日志到下一个START_CKPT为止,START_CKPT之前的日志可以丢弃;如果首先碰到的是START_CKPT, 只需要回放第一步中记录的所有事务最早开始的地方,再之前的日志记录可以直接删除。
REDO LOG
redo log是指在回放日志的时候把已经commit的事务重做一遍,对于没有commit的事务按照abort处理。日志回放并不会处理任何没有commit的事务, 因此,在COMMIT日志持久化之前,不能将数据的修改持久化。因为如果数据在COMMIT之前持久化,那么在系统异常退出的情况下,这种部分修改的 事务就会处于一种不一致状态。同时,由于重做事务,因此事务日志中必须记录事务修改以后的值。redo log必须满足以下规则:
- R1: Before modifying any database element X on disk, it is necessary that all log records pertaining[附属于] to this modification of X. including both the update recordand the record must appear on disk.
redo log与数据修改的顺序为:
- 记录对数据库的修改(update日志)
- 写COMMIT日志
- 修改数据库项
日志回放与checkpoint
对于没有checkpoint的日志回放需要:首先从后往前扫描日志:记录所有已经commit的事务,对于没有commit的事务在日志末尾追加abort; 然后从前往后扫描日志,redo所有已经commit的事务日志。
redo log生成checkpoint的方法:
- 写日志START_CKPT<T1, T2, ..., Tn>;其中Ti表示当前正在进行还没有commit的事务
- 将所有已经commit的事务对数据库数据的修改持久化,这里要求事务一定要在提交COMMIT日志记录之后才能写入缓存。
- 写日志END_CKPT
在从后往前扫描日志文件的时候,如果碰到END_CKPT,我们就知道在START_CKPT之前的所有已经commit的事务都不需要再重做了,因此从后 往前扫描日志可以再START_CKPT中记录的正在进行的事务中最早开始的事务为止。当然,可能通过特定的方式将一个事务的所有日志串接 起来,减少日志的扫描数量。
redo log的简化实例
redlog在nosql系统中的应用比较广泛,主要原因是实现简单。在行级事务的情况下一个事务退化到只有一条日志,这个时候的操作是最简单的:
- 写日志
- 更新数据库
此时存在的问题是更新数据库的步骤失败如何处理?这里有两种方式:
- 采用copy-on-write进行事务预处理(写日志之前就执行了预更新),保证更新数据库一定可以成功
- 在更新数据库失败的情况下,程序退出。
显然,第一种处理方式更加优美和谐。
这里提到了事务预处理,它是优化事务性能很关键的技术。由于对事务的操作往往是串行的,如果对数据的读取也按照存心的方式进行处理, 那么这个系统的tps肯定无法提高。简单的事务预处理就是通过并行的方式读取数据,调高事务前期处理的并发度, 而真正在进行事务处理的时候除写日志以外可以做到完全的内存操作,以此提高系统的tps。
对于这种简化版的redo log的checkpoint就很简单了:
- 在内存中生成snapshot,有很多技术可以在瞬时生成snapshot,生成snapshot的时候记录日志点(checkpoint)
- 将生成的snapshot进行持久化
- 记录checkpoint日志点
在工程实践中,很多技术点需要简单可依赖。
UNDO/REDO LOG
正如前面所说,undo log的缺点十分明显:要求事务在完成以后事务所涉及到的所有数据库数据都马上进行持久化,这样的性能显然是谁 也无法接受的。redo log在nosql系统中得到了很广泛的应用,但是在处理复杂事务的时候仍然会有相应的缺点:由于事务在真正的提交 以前,对数据的更新都必须啊放在事务局部的缓冲区里,这样可能会导致事务占用大量的内存。
另外,在解决分布式一致性问题的时候,存在情况确实是需要回滚的,此时redo log就无能为力了。具体的例子参见本站内对 chubby一种可能实现的分析。
而undo/redo log可以很好的解决这些问题。undo/redo log是指在日志回放的时候像undo log那样回滚所有没有commit的事务; redo log一样redo所有已经commit的事务。由于同时要进行redo和undo,因此日志记录中必须同时记录修改前的值和修改后的值。 <T, X, v, w>,表示事务T修改了元素X的内容,修改之前X的值为v,修改之后值为w。同时undo/redo log对于数据和日志的持久化 顺序要求很低:
- URl Before modifying any database element X on disk because of changes made by some transaction T, it is necessary that the update record <T, X, v, w> appear on disk
就是说,在读数据库进行修改前,必须前必须先写事务更新日志。undo/redo log也被称作write ahead log。
日志与数据更新的过程:
- 记录update日志
- 更新数据库(不一定持久化,写入缓存中)
- 在合适的时候写commit日志
日志回放与checkpoint
没有checkpoint的日志回放很简单:首先从后往前遍历所有日志,undo所有没有commit的事务,并追加abort,同时记录所有 已经提交的事务;然后从前往后遍历日志,redo所有已经提交的事务。
redo/undo log的checkpoint方式与redo log相似:
- 写日志START_CKPT<T1, T2, ..., Tn>;其中Ti表示当前正在进行还没有commit的事务
- 将所有缓存中的脏数据持久化到磁盘
- 写日志END_CKPT
与redo log checkpoint的区别是:在第二步,undo/redo log可以将所有数据持久化到磁盘,包括还没有commit的事务;而 redo log在第二步的时候只能够将所有已经commit的事务的脏数据持久化到磁盘。
在《Database Systems: The Complete Book》一书中,关于undo/redo log的checkpoint有一段这样的描述:
- A transaction must not write any values (even to memory buffers) until it is certain not to abort.
笔者暂时还没有理解这段描述的意思,欢迎大家探讨。我的理解是:因为在生成 checkpoint的第二步,会把所有脏数据持久化到磁盘,那么这个时候所有的更新(即使没有commit的事务的更新)都回被 其他事务看到,如果某个没有commit的事务最终abort了,这样其他的事务就看到了不一致的数据。 如果我的理解是正确的, 那么undo/redo log和redo log一样在事务真正提交之前,对数据的所有更新都必须在事务局部的缓冲区里面,那么undo/redo log相对于redo log而言就没有这方面的优势了。 那么在单机环境下,undo/redo log相对于redo log还有什么优势呢?