Lihang Liu's Homepage

本地事务的原子性和持久性是如何实现的

要实现持久性,数据库事务的修改必须要写入到磁盘中,否则,当出现数据库异常退出、系统宕机、机房断电等意外情况时,位于内存中的所有数据将会丢失。

但是,磁盘的操作又不是一个原子性行为,数据库可能在写入前、写入后、正在写等状态发生异常,这就需要我们采取一定的手段来保证事务的原子性。

目前的数据库管理系统采用的是先写日志、再写磁盘的策略来保证事务的原子性以及持久性的。本文将阐述 Commit Logging、Shadow Paging 以及 Write-Ahead Logging 三种策略。

Commit Logging

Commit logging 是一种事务实现方式。在将数据修改写到磁盘之前,先把事务操作所需要的所有信息(包括修改什么数据、数据物理上位于哪个内存页和磁盘块中、从什么值改成什么值,等等)以日志的形式写到文件中。

只有日志记录全部写入到了磁盘之中后(日记文件也需要持久化到磁盘中),才会根据日志记录中记录的信息将修改写入到磁盘。

一个事务成功提交会,会在日志记录中添加上 Commit Record ,表示事务已经成功提交。在讲事务的修改成功写入到磁盘中后,还会在添加上一条 End Record 用来标识磁盘写入成功。

Commit Logging 如何保证持久性和原子性

一旦事务在日志记录中写入了 Commit Record ,那么该事务的所有修改已经全部安全落盘,那么,即便数据库在讲事务的修改写入到磁盘的过程中异常退出,在数据库系统重启恢复阶段,也能够将未完成的写入继续做完,这保证了事务的持久性。

如果 DBMS 在将一个事务的 Commit Record 安全写入到日志文件之前就异常终止,那么,只需要在 DBMS 重启恢复阶段,回滚该事务的所有变更的写入操作即可,这保证了事务的原子性,不会存在中间状态。

Commit Logging 的缺陷

Commit Logging 的缺陷在于,在事务讲修改更新操作完全写入日志记录文件之前,无法讲修改写到磁盘之中,日志文件的修改和磁盘的修改变成了同步的操作。如果在日志文件写入的同时磁盘处于空闲状态,那么将会导致磁盘资源的空置和浪费。

Shadow Paging

Shadow paging 是另一个实现事务原子性以及持久性的技术。它是基于 Copy-on-Write 的方法。

Shadow paging 是这样工作的:

  1. 在事务修改一个数据页之前,将会复制一份该数据页的副本,该副本即为原数据页的影子 shadow。
  2. 所有的修改将会作用于影子页,因为影子页没有被位于磁盘中的任何数据页所引用,因此它的修改是十分安全的,不会影响一致性。
  3. 当影子页准备被持久化的时候,磁盘中所有引用原数据页的指针将被指向影子页。
  4. 最后一步的指针修改可以认为是原子性的,现在磁盘在硬件上保证了不会出现改了『半个值』的情况。

Shadow Paging 的缺点

Shadow paging 的实现要比 Commit Logging 更加简单,但只要涉及到并发锁时,Shadow Paging 所能够处理的并发事务数量将成为一个瓶颈。

Write-Ahead Logging

我们在描述 Commit Logging 的缺点时提到,Commit Logging 最大的缺陷在于日志记录的写入和磁盘的写入是一个同步操作:只有在日志记录写入完成之后才会开始将修改写入到磁盘,这是对硬件资源的空置和浪费。

如果我们允许在日志记录写入完全之前,就开始将日志记录中的一部分修改写入到磁盘,那将提高硬件资源的使用情况,同时加快整个事务的执行过程,这就是 Write-Ahead Logging 策略。

FORCE and STEAL

我们将事务提交后,修改必须同时完成写入称为 FORCE,不必须同时完成写入称为 NO-FORCE。

我们将事务提交之前,允许修改提前写入到磁盘称为 STEAL,不允许称为 NO-STEAL。

一个事务如果是 NO-FORCE 且 STEAL 的话,那么性能将达到最好:

  1. 如果一个事务已经将修改安全写到了日志文件中,那么我们可以随时随地将修改写到磁盘,而无需立刻就这么做(现实中大多数 DBMS 均采用的 NO-FORCE 策略);
  2. 允许事务将修改在事务提交之前就开始写入到磁盘中,这将提高磁盘的 IO 利用率。

Write-Ahead Logging 如何保证原子性以及持久性

Commit Logging 是 NO-FORCE + NO-STEAL 的策略,因为如果 DBMS 异常退出但修改没有写入完全,那么将提前写入的这些数据就是错误数据。

Write-Ahead Logging 允许 NO-FORCE,也允许 STEAL,它使用了 Undo Log 来保证提前写入的数据的正确性。当数据落盘前,必须先记录 Undo Log,Undo Log 记录了哪个数据页、哪一行的数据从什么值变成了什么值,等等。在 Undo Log 的帮助下,即便发生了异常我们也能够保证数据的正确了。

Write-Ahead Logging 的 DBMS 崩溃恢复时会执行下面 3 个阶段的操作:

  • 分析阶段(Analysis):该阶段从最后一次 checkpoint 开始扫描日志,找出所有没有 End Record 的事务,组成待恢复的事务集合。
  • 重做阶段(Redo):该阶段会将待恢复事务集合中,已经在日志文件中写入了 Commit Record 的事务进行重做,写入完成后再添加上一条 End Record ,然后移除出待恢复事务集合。这些事务的全部修改都已经包含在了日志文件中,可以安全落盘,保证了事务的持久性。
  • 回滚阶段(Undo):待恢复的事务集合中,还剩下那些没有在日志文件中写入 Commit Record ,它们被称为 Loser,根据 Undo Log 中的信息,将已经提前写入磁盘的信息重新改写回去,以达到回滚这些 Loser 事务的目的。这保证了事务的原子性。
NO-STEAL STEAL
NO-FORCE 最快
FORCE 最慢
NO-STEAL STEAL
NO-FORCE 重做日志 重做日志+回滚日志
FORCE 不需要日志 回滚日志

Reference

  1. Shadow paging - Wikipedia
  2. 本地事务 | 凤凰架构
  3. 《MySQL 技术内幕——InnoDB 存储引擎》