数据库恢复 —— 关于事务原子性、持久性
数据的一致性
在数据库系统作为一个单独的软件诞生之前,工程师需要给每一个系统开发管理数据文件的逻辑,一个业务接着一个业务都有类似的在文件中存储数据的需求。“那个时候,开发软件最难的部分不是完成业务的执行逻辑,而是如何保证不同数据文件里数据的一致性。”,Thomas Nies 曾这么说过,于是他创建了 TOTAL 数据库系统,把所有数据处理的逻辑都纳入其中,开始了计算机产业内第一次独立的软件售卖,并且成为了前关系数据库时代最成功的数据库系统之一。
经典的数据一致性问题可以用一笔用户之间的转账操作来解释。
转账操作:
1) 借方账户余额减去 100 元
2) 贷方账户余额加上 100 元
3) 记一条转账记录
一次转账操作需要修改三个数据,第一步从借方账户(转出账户)的余额减去 100 元;第二步从贷方账户(转入账户)的余额加上 100 元;第三步要在记录表里为此次转账操作记一条记录,为了之后的对账、查询等操作,三步修改是三处不同的数据。如果只有第一步完成,在执行第二步之前,机器断电,重启之后系统需要知道断电之前的转账操作是不完整的,借方余额需要恢复原状。否则,借方少了 100 元,贷方却没收到 100 元,这种即数据的不一致。
在计算机系统里,早期是磁带,后来是磁盘,现在还有固态盘,都是可以持久化存储数据的。所谓持久化存储数据,主要是说机器断电后数据还在。但是内存不行,存储在内存中的数据断电后就都消失了。数据库系统就是在统筹这两个存储设备的特性,完成数据的存储和查询。在 IBM 孵化关系数据库的原型系统 System R 时,Jim Gray 提出了要用“事务”来保证数据的原子性(Atomicity)、持久性(Durability)。数据库内的每一组数据修改都以“事务”为接口。原子性保证了事务的多个操作或者都生效或者都不生效,不会存在中间状态。持久性保证一旦事务的操作生效,不会因为任何原因其修改被撤销或丢失。从事务的持久性角度看,事务写入的数据一定要写到硬盘等持久化存储里才能保证持久性,只存储在内存中的数据如果遇到系统突然终止或者系统断电就会丢失。事务修改的数据分散在硬盘上的不同地方时,第一处修改完成时,第二处修改还没开始,这个时候出现系统故障,那么只做了一半的事务就残留在了硬盘上,当系统恢复时需要能够明确事务一半的状态,然后把已经做的修改恢复原状,好像这个事务没发生过一样,这才能保证事务的原子性。
原子性和持久性是事务密切相关的两个属性,数据库系统会保障在任何异常情况下原子性和持久性都能够被保证,诸如程序异常终止、操作系统故障、系统断电等。事务恢复功能(Transaction Recovery)就是用来完成这件事情的。下面的文章就会介绍实现的方法。
事务恢复(Transaction Recovery)
经过了多年的发展后,事务恢复有三种成熟的技术方案,分别是 Commit Logging, Shadow Paging 和 Write Ahead Logging。了解数据库的人应该知道 Write Ahead Logging 方案,尤其是 80 年代末 C. Mohan 发表了著名的 ARIES 论文后,Write Ahead Logging 技术得到了全面加强,ARIES 也成为了数据库领域的经典算法。Wrtie Ahead Logging 也是目前使用最为广泛的一种技术,但是技术细节也很繁复。Commit Logging、Shadow Paging 也一直在被使用,只是很少被提及。下面分别进行介绍。
Commit Logging
Commit Logging 是最朴素的方法,甚至没有一篇专门的论文来论诉它。早期,因为内存很小,Commit Logging 解决不了实际问题,所以没有人去写论文。后来,Write Ahead Logging 非常成熟,也没有必要再为 Commit Logging 写文章了。
本质上 Commit Logging 就是 Write Ahead Logging 在内存特别大时的一种特例,但是 Commit Logging 不需要考虑脏数据(事务进行到一半还未确定提交时已经修改过的数据)持久化的状态,没有 Write Ahead 的行为,也不需要考虑数据和日志之间对应关系,是与 Write Ahead Logging 出发点完全不同的一种解决方法,整体结构比 Write Ahead Logging 简单非常多。简单有一个潜台词就是高效。
使用 Commit Logging 的数据库有 OceanBase、Hekaton(SQL Server 的内存存储引擎)。OceanBase 所有新写入和修改的增量数据都是记在内存中,内存中的增量数据定期和硬盘中的数据进行合并,相当于一个内存存储引擎和硬盘存储引擎的结合。Hekaton 是完全的内存存储引擎。这两者共同的特点是脏数据不会持久化。
日志(Log)是硬盘上的一个数据文件并且只会顺序追加写入,可以用来高效的持久化信息。Commit Logging 就是在事务内所有操作都结束开始执行提交(Commit)时,将事务所有修改的数据(包括插入、更新、删除)写入日志,如果所有的日志写入都成功,那么事务最终就是成功的,即使出现系统故障,数据库系统重启后依然可以从日志中恢复出事务所有修改的数据,保证了事务的持久性。那么事务的原子性是如何保证的呢?如果日志只写到一半没有完全写完时出现系统故障,首先对于内存数据库来说,事务对应的修改都在内存中,系统故障后这些修改也都随着内存内容的丢失一起消失了。其次,当数据库系统重新恢复时,会发现日志中事务只写了一部分并没有完成标记,那么这个事务就记为回滚状态,并不会重放到内存中,那么这个事务就好像完全没有发生过一样。
如上图,如果数据库中的两个账户 A、B 的余额分别是 800 和 400,进行 100 元的转账操作的事务会记录的日志是:
A 的余额是 700
B 的余额是 500
转账记录:A 转账 100 元给 B
END
当上面的日志内容成功写入日志文件后,事务就可以宣布执行成功了。
Shadow Paging
世界上实例数量最多的数据库是 SQLite,SQLite Version 3 使用的就是 Shadow Paging 的事务恢复机制。
Shadow Paging 和日志完全没有关系,Shadow Paging 方案中,事务操作时新修改的数据直接写到硬盘的数据中,但是并不是直接就地修改原先的数据,而是会把之前的数据复制出来,保留原先的数据不动,修改复制出的数据。在事务操作过程中,被修改的数据会同时存在两份,一份修改前的数据,一份是修改后的数据,这就是影子(Shadow)这个名字的由来。
事务的修改是直接持久化在硬盘上,持久性的保证很直观。Shadow Paging 如何保证事务的原子性呢?其原理就是在事务执行过程中修改的数据虽然以拷贝出的新的一份数据写入到硬盘中,但是这些新的写入在事务提交前并没有生效,当事务提交时,才以一次原子的数据写入让整个事务新的修改生效。事务提交时的原子修改是因为只修改一处很少的数据,所以可以由硬盘等持久化设备来保证这次写入的原子性。最终达成整个事务的原子性。如果在事务提交的操作执行之前,出现系统故障,数据库恢复时是见不到刚才未完成事务的修改的,就好像这个事务没有发生过。硬盘上的这个事务曾经修改的数据后期也会由垃圾回收模块回收重用。
如上图,还以转账事务为例。A 和 B 的余额都是直接写入新的位置,保证原先的数据没有改动。系统通过两个目录结构分别指向修改前的数据和修改后的数据,最后 Current 指针原子切换到新的目录上,表示事务提交成功。
Shadow Paging 方案事务的并发能力受限制,但是,Shadow Paging 相比基于日志的方案,事务修改的数据是直接落盘的,不需要在日志和数据里写两份,所以大部分数据库系统虽然没有直接使用 Shadow Paging 方案,但是在存储 LOB(Large OBject)数据时一般会结合 Shadow Paging 和另外一种事务恢复方案。使用 Shadow Paging 直接写 LOB,将指向 LOB 的指针作为数据内容记日志,这样可以避免大块的 LOB 数据在日志和数据中写两遍,优化 LOB 的处理能力。
Write Ahead Logging
Write Ahead Logging 也是使用日志(Log)来支持事务的恢复,与之前描述的 Commit Logging 最大的不同是 Write Ahead Logging 不假定事务的修改在事务提交之前可以一直保存在内存中,换句话说,事务执行过程中已经修改过的数据允许写到硬盘等持久化存储中。这点是符合事务技术最初发展阶段(70、80 年代)的计算机硬件环境,当时机器的内存很小。
使用 Write Ahead Logging 的数据库有 Oracle, SQL Server, MySQL InnoDB 等常见的传统数据库系统。ARIES 也已经是基于硬盘的数据库实现事务恢复的标准方案。
Write Ahead Logging 面对的场景是事务执行修改的数据也是先记录在内存的缓存中,如果内存不足,修改后的数据会直接持久化在硬盘中。但是之后事务可能需要回滚,所以日志中需要记录两类信息,一类是事务做了哪些修改,被称为 REDO;另一类是事务修改之前数据是什么样子,被称为 UNDO。事务的持久性还是由事务对应的 REDO 日志部分完全持久化成功来保证。事务的原子性由日志的 UNDO 部分保证,如果事务进展到一半有一部分修改过的数据已经持久化了,出现系统故障,数据库重启之后会根据 UNDO 里面记录的信息将事务曾经做过的修改恢复原状,恢复到事务没发生之前的样子。为了保证 UNDO 的完整性,不能出现有数据对应的 UNDO 日志还没有,所以日志要在数据落盘之前持久化,这也就是 Write Ahead 名字的由来。
还以上面的转账操作为例,Write Ahead Logging 需要记录的日志是:
A 原始余额是 800,修改后的余额是 700
B 原始余额是 400,修改后的余额是 500
转账记录:A 转账 100 元给 B
END
本文转载自公众号OceanBase