乐趣区

MySQL探秘(八):InnoDB的事务

 事务是数据库最为重要的机制之一,凡是使用过数据库的人,都了解数据库的事务机制,也对 ACID 四个基本特性如数家珍。但是聊起事务或者 ACID 的底层实现原理,往往言之不详,不明所以。所以,今天我们就一起来分析和探讨 InnoDB 的事务机制,希望能建立起对事务底层实现原理的具体了解。

 数据库事务具有 ACID 四大特性。ACID 是以下 4 个词的缩写:

原子性(atomicity):事务最小工作单元,要么全成功,要么全失败。
一致性(consistency):事务开始和结束后,数据库的完整性不会被破坏。
隔离性(isolation):不同事务之间互不影响,四种隔离级别为 RU(读未提交)、RC(读已提交)、RR(可重复读)、SERIALIZABLE(串行化)。
持久性(durability):事务提交后,对数据的修改是永久性的,即使系统故障也不会丢失。

 下面,我们就以一个具体实例来介绍数据库事务的原理,并介绍 InnoDB 是如何实现 ACID 四大特性的。
示例介绍
 我们首先来看一下具体的示例。大家可以自己亲自试验一下,这样理解和记忆都会更加深刻。首先,使用如下的 SQL 语句创建两张表,分别是 goods 和 trade,代表货物和交易。并向 goods 表中插入一条记录,id 为 1 的货物数量为 10。
CREATE TABLE goods (id INT, num INT, PRIMARY KEY(id));
CREATE TABLE trade (id INT, goods_id INT, user_id INT, PRIMARY KEY(id));
INSERT INTO goods VALUES(1, 10);
 然后打开终端,连接数据库,开启会话一,先用 BEGIN 显示开启一个事务。会话一先将 goods 表中 id 为 1 的货物的数量减一,然后向 trade 表中添加一笔交易的记录,最后使用 COMMIT 显示提交事务。而会话二则先查询 goods 表中 id 为 1 的货物数量,然后向 trade 表中添加一笔交易记录,接着更新 goods 表中 id 为 1 的货物的数量,最后使用 ROLLBACK 进行事务的回滚。其中,两个会话中执行的具体语句和先后顺序如下图所示。

 这个示例可以体现数据库事务的很多特性,我们一一来介绍。首先会话一的操作 2 更新了 id 为 1 的货物的数量,但是会话二的操作 5 读出来的数量仍然是 10,这体现了事务的隔离性,使用 InnoDB 的多版本控制机制实现。
 会话二的操作 7 也要更新同种货物的数量,此时因为会话一的操作 2 已经更新了该货物的数量,InnoDB 已经锁住了该记录的行锁,所以操作 7 会被阻塞,直到会话一 COMMIT。但是会话一的操作 4 和会话二的操作 7 都是向 trade 表中插入记录,后者却不会因为前者而阻塞,因为二者插入的不是同一行记录。锁机制是一种常见的并发控制机制,它和多版本控制机制一起实现了 InnoDB 事务的隔离性,关于 InnoDB 锁相关的具体内容可以参考 InnoDB 锁的类型和状态查询和 InnoDB 行锁算法。
 会话一事务最终使用 COMMIT 提交了事务而会话二事务则使用 ROLLBACK 回滚了整个事务,这体现了事务的原子性。即事务的一系列操作要么全部执行(COMMIT),要么就全部不执行(ROLLBACK),不存在只执行一部分的情况。InnoDB 使用事务日志系统来实现事务的原子性。这里有的同学就会问了,如果中途连接断开或者 Server Crash 会怎么样。能怎么样,直接自动回滚呗。
 一旦会话一使用 COMMIT 操作提交事务成功后,那么数据一定会被写入到数据库中并持久的存储起来,这体现了事务的持久性。InnoDB 使用 redo log 机制来实现事务的持久性。
 而事务的一致性比较难以理解,简单的讲在事务开始时,此时数据库有一种状态,这个状态是所有的 MySQL 对象处于一致的状态,例如数据库完整性约束正确,日志状态一致等。当事务提交后,这时数据库又有了一个新的状态,不同的数据,不同的索引,不同的日志等。但此时,约束,数据,索引,日志等 MySQL 各种状态还是要保持一致性。也就是说数据库从一个一致性的状态,变到另一个一致性的状态。事务执行后,并没有破坏数据库的完整性约束。
 下面我们就来详细讲解一下上述示例涉及的事务的 ACID 特性的具体实现原理。总结来说,事务的隔离性由多版本控制机制和锁实现,而原子性、一致性和持久性通过 InnoDB 的 redo log、undo log 和 Force Log at Commit 机制来实现。
原子性,持久性和一致性
 原子性,持久性和一致性主要是通过 redo log、undo log 和 Force Log at Commit 机制机制来完成的。redo log 用于在崩溃时恢复数据,undo log 用于对事务的影响进行撤销,也可以用于多版本控制。而 Force Log at Commit 机制保证事务提交后 redo log 日志都已经持久化。开启一个事务后,用户可以使用 COMMIT 来提交,也可以用 ROLLBACK 来回滚。其中 COMMIT 或者 ROLLBACK 执行成功之后,数据一定是会被全部保存或者全部回滚到最初状态的,这也体现了事务的原子性。但是也会有很多的异常情况,比如说事务执行中途连接断开,或者是执行 COMMIT 或者 ROLLBACK 时发生错误,Server Crash 等,此时数据库会自动进行回滚或者重启之后进行恢复。
 我们先来看一下 redo log 的原理,redo log 顾名思义,就是重做日志,每次数据库的 SQL 操作导致的数据变化它都会记录一下,具体来说,redo log 是物理日志,记录的是数据库页的物理修改操作。如果数据发生了丢失,数据库可以根据 redo log 进行数据恢复。
 InnoDB 通过 Force Log at Commit 机制实现事务的持久性,即当事务 COMMIT 时,必须先将该事务的所有日志都写入到 redo log 文件进行持久化之后,COMMIT 操作才算完成。当事务的各种 SQL 操作执行时,即会在缓冲区中修改数据,也会将对应的 redo log 写入它所属的缓存。当事务执行 COMMIT 时,与该事务相关的 redo log 缓冲必须都全部刷新到磁盘中之后 COMMIT 才算执行成功。

 redo log 写入磁盘时,必须进行一次操作系统的 fsync 操作,防止 redo log 只是写入了操作系统的磁盘缓存中。参数 innodb_flush_log_at_trx_commit 可以控制 redo log 日志刷新到磁盘的策略,它的具体作用可以查阅 InnoDB 的磁盘文件及落盘机制
 redo log 全部写入磁盘后事务就算 COMMIT 成功了,但是此时事务修改的数据还在内存的缓冲区中,称其为脏页,这些数据会依据检查点 (CheckPoint) 机制择时刷新到磁盘中,然后删除相应的 redo log,但是如果在这个过程中数据库 Crash 了,那么数据库重启时,会依据 redo log file 将那些还在内存中未更新到磁盘上的数据进行恢复。
 数据库为了提高性能,数据页在内存修改后并不是每次都会刷到磁盘上。而是引入 checkpoint 机制,择时将数据页落盘,checkpoint 记录之前的数据页保证一定落盘了,这样相关的 redo log 就没有用了(由于 InnoDB redo log file 循环使用,这时这部分日志就可以被覆盖),checkpoint 之后的数据页有可能落盘,也有可能没有落盘,所以 checkpoint 之后的 redo log file 在崩溃恢复的时候还是需要被使用的。InnoDB 会依据脏页的刷新情况,定期推进 checkpoint,从而减少数据库崩溃恢复的时间。检查点的信息在第一个日志文件的头部。
 数据库崩溃重启后需要从 redo log 中把未落盘的脏页数据恢复出来,重新写入磁盘,保证用户的数据不丢失。当然,在崩溃恢复中还需要回滚没有提交的事务。由于回滚操作需要 undo 日志的支持,undo 日志的完整性和可靠性需要 redo 日志来保证,所以崩溃恢复先做 redo 恢复数据,然后做 undo 回滚。
 在事务执行的过程中,除了记录 redo log,还会记录一定量的 undo log。undo log 记录了数据在每个操作前的状态,如果事务执行过程中需要回滚,就可以根据 undo log 进行回滚操作。

 undo log 的存储不同于 redo log,它存放在数据库内部的一个特殊的段 (segment) 中,这个段称为回滚段。回滚段位于共享表空间中。undo 段中的以 undo page 为更小的组织单位。undo page 和存储数据库数据和索引的页类似。因为 redo log 是物理日志,记录的是数据库页的物理修改操作。所以 undo log 的写入也会产生 redo log,也就是 undo log 的产生会伴随着 redo log 的产生,这是因为 undo log 也需要持久性的保护。如上图所示,表空间中有回滚段和叶节点段和非叶节点段,而三者都有对应的页结构。
 我们再来总结一下数据库事务的整个流程,如下图所示。

 事务进行过程中,每次 sql 语句执行,都会记录 undo log 和 redo log,然后更新数据形成脏页,然后 redo log 按照时间或者空间等条件进行落盘,undo log 和脏页按照 checkpoint 进行落盘,落盘后相应的 redo log 就可以删除了。此时,事务还未 COMMIT,如果发生崩溃,则首先检查 checkpoint 记录,使用相应的 redo log 进行数据和 undo log 的恢复,然后查看 undo log 的状态发现事务尚未提交,然后就使用 undo log 进行事务回滚。事务执行 COMMIT 操作时,会将本事务相关的所有 redo log 都进行落盘,只有所有 redo log 落盘成功,才算 COMMIT 成功。然后内存中的数据脏页继续按照 checkpoint 进行落盘。如果此时发生了崩溃,则只使用 redo log 恢复数据。
隔离性
 InnoDB 事务的隔离性主要通过多版本控制机制和锁机制实现,具体可以参考多版本控制,InnoDB 锁的类型和状态查询和 InnoDB 行锁算法三篇文章。
后记
 本来想一篇文章将 MySQL 的事务机制讲明白,写完自己读了一遍,还是发现内容有些晦涩难懂,复杂的知识本来就是很难讲明白的,夫夷以近,则游者众;险以远,则至者少,希望读者以本文作为一篇指引性的文章,自己再去更加深入的地方去探秘。不过,能将复杂知识讲解的通俗简单也是一项很大的本领,文字和讲解能力还是需要提示的。

Mysql 探索(一):B-Tree 索引
数据库内部存储结构探索
MySQL 探秘(二):SQL 语句执行过程详解
MySQL 探秘(三):InnoDB 的内存结构和特性
MySQL 探秘(四):InnoDB 的磁盘文件及落盘机制
MySQL 探秘(五):InnoDB 锁的类型和状态查询
MySQL 探秘(六):InnoDB 一致性非锁定读

参考

MySQL · 引擎特性 · InnoDB 事务系统
MySQL · 引擎特性 · InnoDB 崩溃恢复过程

退出移动版