写点什么

MySQL 中事务的持久性实现原理

用户头像
X先生
关注
发布于: 2020 年 11 月 02 日

前言

说到数据库事务,大家脑子里一定很容易蹦出一堆事务的相关知识,如事务的 ACID 特性,隔离级别,解决的问题(脏读,不可重复读,幻读)等等,但是可能很少有人真正的清楚事务的这些特性又是怎么实现的,为什么要有四个隔离级别。

在之前的文章我们已经了解了MySQL中事务的隔离性的实现原理,今天就继续来聊一聊 MySQL 持久性的实现原理。

当然 MySQL 博大精深,文章疏漏之处在所难免,欢迎批评指正。

说明

MySQL 的事务实现逻辑是位于引擎层的,并且不是所有的引擎都支持事务的,下面的说明都是以 InnoDB 引擎为基准。

InnoDB 读写数据原理

在往下学习之前,我们需要先来了解下 InnoDB 是怎么来读写数据的。我们知道数据库的数据都是存放在磁盘中的,然后我们也知道磁盘 I/O 的成本是很大的,如果每次读写数据都要访问磁盘,数据库的效率就会非常低。为了解决这个问题,InnoDB 提供了 Buffer Pool 作为访问数据库数据的缓冲。

Buffer Pool 是位于内存的,包含了磁盘中部分数据页的映射。当需要读取数据时,InnoDB 会首先尝试从 Buffer Pool 中读取,读取不到的话就会从磁盘读取后放入 Buffer Pool;当写入数据时,会先写入 Buffer Pool 的页面,并把这样的页面标记为 dirty,并放到专门的 flush list 上,这些修改的数据页会在后续某个时刻被刷新到磁盘中(这一过程称为刷脏,由其他后台线程负责) 。如下图所示:


这样设计的好处是可以把大量的磁盘 I/O 转成内存读写,并且把对一个页面的多次修改 merge 成一次 I/O 操作(刷脏一次刷入整个页面),避免每次读写操作都访问磁盘,从而大大提升了数据库的性能。

持久性定义

持久性是指事务一旦提交,它对数据库的改变就应该是永久性的,接下来的其他操作或故障不应该对本次事务的修改有任何影响。

通过前面的介绍,我们知道 InnoDB 使用 Buffer Pool 来提高读写的性能。但是 Buffer Pool 是在内存的,是易失性的,如果一个事务提交了事务后,MySQL 突然宕机,且此时 Buffer Pool 中修改的数据还没有刷新到磁盘中的话,就会导致数据的丢失,事务的持久性就无法保证。

为了解决这个问题,InnoDB 引入了 redo log 来实现数据修改的持久化。当数据修改时,InnoDB 除了修改 Buffer Pool 中的数据,还会在 redo log 记录这次操作,并保证 redo log 早于对应的页面落盘(一般在事务提交的时候),也就是常说的 WAL。若 MySQL 突然宕机了且还没有把数据刷回磁盘,重启后,MySQL 会通过已经写入磁盘的 redo log 来恢复没有被刷新到磁盘的数据页。

实现原理:redo log

为了提高性能,和数据页类似,redo log 也包括两部分:一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。redo log 是物理日志,记录的是数据库中物理页的情况 。

当数据发生修改时,InnoDB 不仅会修改 Buffer Pool 中的数据,也会在 redo log buffer 记录这次操作;当事务提交时,会对 redo log buffer 进行刷盘,记录到 redo log file 中。如果 MySQL 宕机,重启时可以读取 redo log file 中的数据,对数据库进行恢复。这样就不需要每次提交事务都实时进行刷脏了。

写入过程


注意点:

  • 先修改 Buffer Pool,后写 redo log buffer。

  • redo 日志比数据页先写回磁盘:事务提交的时候,会把 redo log buffer 写入 redo log file,写入成功才算提交成功(也有其他场景触发写入,这里就不展开了),而 Buffer Pool 的数据由后台线程在后续某个时刻写入磁盘。

  • 刷脏的时候一定会保证对应的 redo log 已经落盘了,也即是所谓的 WAL(预写式日志),否则会有数据丢失的可能性。


好处

事务提交的时候,写入 redo log 相比于直接刷脏的好处主要有三点:

  • 刷脏是随机 I/O,但写 redo log 是顺序 I/O,顺序 I/O 可比随机 I/O 快多了,不需要。

  • 刷脏是以数据页(Page)为单位的,即使一个 Page 只有一点点修改也要整页写入;而 redo log 中只包含真正被修改的部分,数据量非常小,无效 IO 大大减少。

  • 刷脏的时候可能要刷很多页的数据,无法保证原子性(例如只写了一部分数据就失败了),而 redo log buffer 向 redo log file 写 log block,是按 512 个字节,也就是一个扇区的大小进行写入,扇区是写入的最小单位,因此可以保证写入是必定成功的。


先写 redo log 还是先修改数据

一次 DML 可能涉及到数据的修改和 redo log 的记录,那它们的执行顺序是怎么样的呢?网上的文章有的说先修改数据,后记录 redo log,有的说先记录 redo log,后改数据,那真实的情况是如何呢?

首先通过上面的说明我们知道,redo log buffer 在事务提交的时候就会写入 redo log file 的,而刷脏则是在后续的某个时刻,所以可以确定的是先记录 redo log,后修改 data page(WAL 当然是日志先写啦)。

那接下来的问题就是先写 redo log buffer 还是先修改 Buffer Pool 了。要了解这个问题,我们先要了解 InnoDB 中,一次 DML 的执行过程是怎么样的。一次 DML 的执行过程涉及了数据的修改,加锁,解锁,redo log 的记录和 undo log 的记录等,也是需要保证原子性的,而 InnoDB 通过 MTR(Mini-transactions)来保证一次 DML 操作的原子性。

首先来看 MTR 的定义:

An internal phase of InnoDB processing, when making changes at the physical level to internal data structures during DML operations. A Mini-transactions (mtr) has no notion of rollback; multiple Mini-transactionss can occur within a single transaction. Mini-transactionss write information to the redo log that is used during crash recovery. A Mini-transactions can also happen outside the context of a regular transaction, for example during purge processing by background threads.

https://dev.mysql.com/doc/refman/8.0/en/glossary.html


MTR 是一个短原子操作,不能回滚,因为它本身就是原子的。数据页的变更必须通过 MTR,MTR 会把 DML 操作对数据页的修改记录到 redo log 里。

下面来简单看下 MTR 的过程:

  • MTR 初始化的时候会初始化一份 mtr_buf

  • 当修改数据时,在对内存 Buffer Pool 中的页面进行修改的同时,还会生成 redo log record,保存在 mtr_buf 中

  • 在执行 mtr_commit 函数提交本 MTR 的时候,会将 mtr_buf 中的 redo log record 更新到 redo log buffer 中,同时将脏页添加到 flush list,供后续刷脏使用。在 log buffer 中,每接收到 496 字节的 log record,就将这组 log record 包装一个 12 字节的 block header 和一个 4 字节的 block tailer,成为一个 512 字节的 log block,方便刷盘的时候对齐 512 字节刷盘。


由此可见,InnoDB 是先修改 Buffer Pool,后写 redo log buffer 的。

恢复数据的过程

在任何情况下,InnoDB 启动时都会尝试执行 recovery 操作。在恢复过程中,需要 redo log 参与,而如果还开启了 binlog,那就还需要 binlog、undo log 的参与。因为有可能数据已经写入 binlog 了,但是 redo log 还没有刷盘的时候数据库就奔溃了(事务是 InnoDB 引擎的特性,修改了数据不一定提交了,而 binlog 是 MySQL 服务层的特性,修改数据就会记录了),这时候就需要 redo log,binlog 和 undo log 三者的参与来判断是否有还没提交的事务,未提交的事务进行回滚或者提交操作。

下面来简单说下仅利用 redo log 恢复数据的过程:

  • 启动 InnoDB 时,找到最近一次 Checkpoint 的位置,利用 Checkpoint LSN 去找大于该 LSN 的 redo log 进行日志恢复。

  • 如果中间恢复失败了也没影响,再次恢复的时候还是从上次保存成功的 Checkpoint 的位置继续恢复。


Recover 过程:故障恢复包含三个阶段:Analysis,Redo 和 Undo。Analysis 阶段的任务主要是利用 Checkpoint 及 Log 中的信息确认后续 Redo 和 Undo 阶段的操作范围,通过 Log 修正 Checkpoint 中记录的 Dirty Page 集合信息,并用其中涉及最小的 LSN 位置作为下一步 Redo 的开始位置 RedoLSN。同时修正 Checkpoint 中记录的活跃事务集合(未提交事务),作为 Undo 过程的回滚对象;Redo 阶段从 Analysis 获得的 RedoLSN 出发,重放所有的 Log 中的 Redo 内容,注意这里也包含了未 Commit 事务;最后 Undo 阶段对所有未提交事务利用 Undo 信息进行回滚,通过 Log 的 PrevLSN 可以顺序找到事务所有需要回滚的修改。


具体见 http://catkang.github.io/2019/01/16/crash-recovery.html


什么是 LSN?

LSN 也就是 log sequence number,也日志的序列号,是一个单调递增的 64 位无符号整数。redo log 和数据页都保存着 LSN,可以用作数据恢复的依据。LSN 更大的表示所引用的日志记录所描述的变化发生在更后面。

什么是 Checkpoint?

Checkpoint 表示一个保存点,在这个点之前的数据页的修改(log LSN<Checkpoint LSN)都已经写入磁盘文件了。InnoDB 每次刷盘之后都会记录 Checkpoint,把最新的 redo log LSN 记录到 Checkpoint LSN 里,方便恢复数据的时候作为起始点的判断。


觉得不错的话请帮忙收藏点赞~


发布于: 2020 年 11 月 02 日阅读数: 425
用户头像

X先生

关注

还未添加个人签名 2019.01.29 加入

腾讯TEG研发管理部小小后台攻城狮一枚,负责腾讯敏捷产品研发平台TAPD的基础功能的开发和维护,热爱技术,喜欢分享,欢迎与我交流~

评论

发布
暂无评论
MySQL中事务的持久性实现原理