MySQL事务实现原理
通常,我们说MySQL事务实际是通过MySQL InnoDB存储引擎实现。所以通过学习InnoDB架构及事务处理方式,让我们更好的理解ACID、隔离性等理论知识。
存储引擎是负责数据的存储和提取的。MySQL支持多种存储引擎,存储引擎以插件方式运行,每个存储引擎之间可以相互替换。
InnoDB简介
InnoDB
是一种兼顾高可靠性和高性能的通用存储引擎。在 MySQL 8.0 中,InnoDB
是默认的 MySQL 存储引擎。除非您配置了不同的默认存储引擎,否则发出CREATE TABLE
不带ENGINE
子句的语句会创建一个InnoDB
表。
InnoDB有以下几个主要优势:
- 其DML(
Data Manipulation Language
)遵循ACID事务模型,通过事务的提交及回滚以及系统崩溃后恢复能力保护用户数据 - 其行级锁、以及Oracle风格的数据一致性读取提升了多用户的并发性和性能
InnoDB和ACID模型
原子性
ACID 模型的原子性方面主要涉及InnoDB
事务。相关的 MySQL 功能包括:
autocommit
设置COMMIT
语句ROLLBACK
语句
- 我们在执行一条“增删改”语句的时候,虽然没有输入
begin
开启事务、commit
提交事务,但是MySQL会隐式开启事务来执行数据更新语句,执行完会自动提交事务。所以通常我们能看到,执行完“增删改”语句后,在数据库中能立马看到执行结果。MySQL中是否能自动提交,是由autocommit
参数决定的,默认是开启。- 通过undo log回滚到事务执行之前的数据
一致性
ACID 模型的一致性方面主要涉及InnoDB
保护数据不崩溃的内部处理。相关的 MySQL 功能包括:
- 双
InnoDB
写缓冲区。请参见 第 15.6.4 节,“双写缓冲区”。 InnoDB
崩溃恢复。请参阅 InnoDB 崩溃恢复。
隔离性
ACID 模型的隔离方面主要涉及InnoDB
事务,特别是适用于每个事务的隔离级别。相关的 MySQL 功能包括:
autocommit
设置 。- 事务隔离级别和
SET TRANSACTION
声明。请参阅 第 15.7.2.1 节,“事务隔离级别”。
MySQL默认隔离级别:可重读读(Repatable Read).
持久性
ACID 模型的持久性方面涉及特定硬件配置交互的 MySQL 软件功能。
- 双
InnoDB
写缓冲区。请参见 第 15.6.4 节,“双写缓冲区”。 innodb_flush_log_at_trx_commit
变量 。sync_binlog
变量 。innodb_file_per_table
变量 。- 备份策略,例如备份频率和类型,以及备份保留期。
- 其他(…)
MVCC
MVCC是多版本并发控制
MVCC (MultiVersion Concurrency Control) 叫做多版本并发控制。它是通过保存数据在某个时间点的快照来实现并发控制的。也就是说,不管事务执行多长时间,事务内部看到的数据是不受其它事务影响的,根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。
简单来说,多版本并发控制 的思想就是保存数据的历史版本,通过对数据行的多个版本管理来实现数据库的并发控制。这样我们就可以通过比较版本号决定数据是否显示出来,读取数据的时候不需要加锁也可以保证事务的隔离效果。
快照读与当前读
在 MVCC 并发控制中,读操作可以分为两类: 快照读(Snapshot Read
)与当前读 (Current Read
)。
- 快照读:读取的是记录的可见版本(有可能是历史版本),不用加锁。 简单的select操作,属于快照读,不需要加锁。
- 当前读:读取的是记录的最新版本,并且当前读返回的记录,都会加锁,保证其他事务不会再并发修改这条记录。
InnoDB下MVCC多版本实现
MySQL InnoDB 存储引擎,实现的是基于多版本的并发控制协议——MVCC,而不是基于锁的并发控制。MVCC 只在 Read Commited 和 Repeatable Read 两种隔离级别下工作
事务版本号
每开启一个事务,我们都会从数据库中获得一个事务 ID(也就是事务版本号),这个事务 ID 是自增长的,通过 ID 大小,我们就可以判断事务的时间顺序。
行记录的隐藏列
InnoDB的 MVCC ,是通过在每行记录的后面隐藏一些重要字段:
DB_ROW_ID
:6-byte,隐藏的行 ID,用来生成默认聚簇索引。如果我们创建数据表的时候没有指定聚簇索引,这时 InnoDB 就会用这个隐藏 ID 来创建聚集索引。采用聚簇索引的方式可以提升数据的查找效率。DB_TRX_ID
:6-byte,操作这个数据的事务 ID,也就是最后一个对该数据进行插入或更新的事务 ID。DB_ROLL_PTR
:7-byte,回滚指针,也就是指向这个记录的 Undo Log 信息。
如下图所示, F1~F6 是表中字段的名字,1~6 是其对应的数据。后面三个隐含字段分别对应该行的隐含ID、事务号和回滚指针
数据更新
首先,假如这条数据是刚 INSERT 的,可以认为 ID 为 1,其他两个字段为空。
然后,当事务 1 更改该行的数据值时,会进行如下操作,如下图所示
- 把该行修改前的值复制到 Undo log,即图中下面的行;
- 修改当前行的值,填写事务编号,使回滚指针指向 Undo log 中修改前的行
接下来,与事务 1 相同,此时 Undo log 中有两行记录,并且通过回滚指针连在一起。因此,如果 Undo log 一直不删除,则会通过当前记录的回滚指针回溯到该行创建时的初始内容,所幸的是在 InnoDB 中存在 purge 线程,它会查询那些比现在最老的活动事务还早的 Undo log,并删除它们,从而保证 Undo log 文件不会无限增长,如下图所示
WAL
WAL,全称是Write-Ahead Logging, 预写日志系统。指的是 MySQL 的写操作并不是立刻更新到磁盘上,而是先记录在日志上,然后在合适的时间再更新到磁盘上。这样的好处是错开高峰期。日志主要分为 undo log、redo log、binlog。
那么为什么不直接更改磁盘中的数据,而是要内存中更改,然后再写日志,最后再落盘?
MySQL之所以不直接把数据写到磁盘中,最主要是性能问题。WAL
的方式是追加操作, 所以磁盘操作是顺序写,而直接更新磁盘写入数据需要先找到写入位置,然后才写到磁盘,所以磁盘操作是随机写。
磁盘的「顺序写 」比「随机写」 高效的多,因此 写入磁盘的开销更小。
SQL执行流程
接下来,让我们通过一个SQL执行,逐步了解基于这套架构下InnoDB事务执行机制。
上图执行一条数据更新SQL,整体流程如下:
-
客户端先通过连接器建立连接
-
解析器会通过词法分析识别出关键字update,表名等等,构建出语法树,接着还会做语法分析,判断输入的语句是否符合MySQL语法结构
-
接着预处理器会判断表和字段是否存在
-
优化器确定执行计划,因为where条件中id是主键索引,所以决定使用id这个索引
-
执行器调用InnoDB存储引擎接口,执行SQL语句
-
存储引擎先从磁盘文件中加载数据到缓冲中,然后写入
undo log
(回滚日志) -
InnoDB存储引擎更新数据至
Buffer pool
缓冲页,缓冲页变更为脏页。等待IO线程定期将数据刷入磁盘 -
记录数据页修改内容更新到
redo log buffer
,然后定期持久化redo log
(重做日志)至磁盘 -
提交事务,产生事务执行日志写入binlog文件
-
IO线程定期更新buffer poll脏页数据写入磁盘
可以看到事务更新语句操作涉及到undo log、redo log、binlog日志,那么事务是如何通过这些日志实现ACID、避免并发事务存在脏读、不可重复读、幻读以问题,以及系统出现故障数据恢复?
- undo log(回滚日志):是 Innodb 存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和通过MVCC实现事务隔离。
- redo log(重做日志):是 Innodb 存储引擎层生成的日志,实现了事务中的持久性,主要用于掉电等故障恢复(系统崩溃时候,脏页数据可能没有持久化,但是redo log已持久化。MySQL重启后,可以根据redo log内容,将所有数据恢复到最新状态);
- binlog (归档日志):是 Server 层生成的日志,主要用于数据备份和主从复制;
为什么需要Buffer Pool?
设计原理
MySQL 的数据都是存在磁盘中的,那么我们要更新一条记录的时候,得先要从磁盘读取该记录,然后在内存中修改这条记录。那修改完这条记录是选择直接写回到磁盘,还是选择缓存起来呢?当然是缓存起来好,这样下次有查询语句命中了这条记录,直接读取缓存中的记录,就不需要从磁盘获取数据了。
为此,Innodb 存储引擎设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。

-
当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取。
-
当修改数据时,如果数据存在于 Buffer Pool 中,那直接修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页(该页的内存数据和磁盘上的数据已经不一致),为了减少磁盘I/O,不会立即将脏页写入磁盘,后续由后台线程选择一个合适的时机将脏页写入到磁盘。
Buffer Pool缓存内容
InnoDB 会把存储的数据划分为若干个「页」,以页作为磁盘和内存交互的基本单位,一个页的默认大小为 16KB。因此,Buffer Pool 同样需要按「页」来划分。
在 MySQL 启动的时候,InnoDB 会为 Buffer Pool 申请一片连续的内存空间,然后按照默认的16KB
的大小划分出一个个的页, Buffer Pool 中的页就叫做缓存页。此时这些缓存页都是空闲的,之后随着程序的运行,才会有磁盘上的页被缓存到 Buffer Pool 中。
所以,MySQL 刚启动的时候,你会观察到使用的虚拟内存空间很大,而使用到的物理内存空间却很小,这是因为只有这些虚拟内存被访问后,操作系统才会触发缺页中断,申请物理内存,接着将虚拟地址和物理地址建立映射关系。
Buffer Pool 除了缓存「索引页」和「数据页」,还包括了 Undo 页,插入缓存、自适应哈希索引、锁信息等等。
为什么需要undo log ?
设计原理
InnoDB存储引擎每次在事务执行过程中,都记录下回滚时需要的信息到一个日志里,那么在事务执行中途发生了 MySQL 崩溃后,就不用担心无法回滚到事务之前的数据,我们可以通过这个日志回滚到事务之前的数据。
事务回滚
undo log 是一种用于撤销回退的日志。在事务没提交之前,MySQL 会先记录更新前的数据到 undo log 日志文件里面,当事务回滚时,可以利用 undo log 来进行回滚。
每当 InnoDB 引擎对一条记录进行操作(修改、删除、新增)时,要把回滚时需要的信息都记录到 undo log 里,比如:
- 在插入一条记录时,要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的记录删掉就好了;
- 在删除一条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了;
- 在更新一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值就好了。
在发生回滚时,就读取 undo log 里的数据,然后做原先相反操作。比如当 delete 一条记录时,undo log 中会把记录中的内容都记下来,然后执行回滚操作的时候,就读取 undo log 里的数据,然后进行 insert 操作。
不同的操作,需要记录的内容也是不同的,所以不同类型的操作(修改、删除、新增)产生的 undo log 的格式也是不同的。
为什么需要redo log?
设计原理
前面说过,InnoDB存储引擎通过Buffer Pool 是提高了读写效率。但是问题来了,Buffer Pool 是基于内存的,而内存总是不可靠,万一断电重启,还没来得及落盘的脏页数据就会丢失。
为了防止断电导致数据丢失的问题,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log 里面,并更新内存。同时,InnoDB 引擎会在适当的时候,由后台线程将缓存在 Buffer Pool 的脏页刷新到磁盘里。
事务恢复
通过redo log和 WAL 技术,InnoDB 就可以保证即使数据库发生异常重启,之前已提交的记录都不会丢失,这个能力称为 crash-safe(崩溃恢复)。可以看出来, redo log 保证了事务四大特性中的持久性。
undo log和redo log区别
这两种日志是属于 InnoDB 存储引擎的日志,它们的区别在于:
-
undo log 记录了此次事务**「开始前」的数据状态,记录的是更新之「前」**的值;事务提交之前发生了崩溃,重启后会通过undo log回滚事务。
-
redo log 记录了此次事务**「完成后」的数据状态,记录的是更新之「后」**的值;事务提交之后发生了崩溃,重启后会通过redo log恢复事务。

为什么需要binlog?
设计原理
MySQL 在完成一条更新操作后,Server 层还会生成一条 binlog,等之后事务提交的时候,会将该事物执行过程中产生的所有 binlog 统一写 入 binlog 文件。
binlog 文件是记录了所有数据库表结构变更和表数据修改的日志,不会记录查询类的操作,比如 SELECT 和 SHOW 操作。
binlog 文件保存的是全量的日志,也就是保存了所有数据变更的情况,理论上只要记录在 binlog 上的数据,都可以恢复,所以如果不小心整个数据库的数据被删除了,得用 binlog 文件恢复数据。
为什么有了 binlog, 还要有 redo log
这个问题跟 MySQL 的时间线有关系。最开始 MySQL 里并没有 InnoDB 引擎,MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。
而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用 redo log 来实现 crash-safe 能力。
简单总结下:
- binlog 是 MySQL 的 Server 层实现的日志,所有存储引擎都可以使用, 用于备份恢复、主从复制;
- redo log 是 Innodb 存储引擎实现的日志,用于掉电等故障恢复。
Server 层有自己的日志,包括错误日志(error log)、二进制日志(binlog)、慢查询日志(slow query log)、查询日志(log)
主从复制
MySQL 的主从复制依赖于 binlog ,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上。复制的过程就是将 binlog 中的数据从主库传输到从库上。
这个过程一般是异步的,也就是主库上执行事务操作的线程不会等待复制 binlog 的线程同步完成。
MySQL 集群的主从复制过程梳理成 3 个阶段:
- 写入 Binlog:主库写 binlog 日志,提交事务,并更新本地存储数据。
- 同步 Binlog:把 binlog 复制到所有从库上,每个从库把 binlog 写到暂存日志中。
- 回放 Binlog:回放 binlog,并更新存储引擎中的数据。
内容小结
MySQL InnoDB存储引擎对事务执行过程中的ACID、事务隔离性、并发事务问题(脏读、不可重复读、幻读)、故障恢复等问题。提供了对应解决方案:
- 通过undo log和MVCC机制满足事务原子性、一致性、隔离性
- 通过redo log和binlog满足事务一致性、持久性
- 通过redo log提供了 crash-safe(崩溃恢复)能力
由于篇幅所限,本文没有对MVCC和undo log是如何实现事务隔离性进行深入研究,如果感兴趣读者可以看文章事务隔离级别实现
参考文档
https://dev.mysql.com/doc/refman/8.0/en/innodb-storage-engine.html
https://www.cnblogs.com/xiaolincoding/p/16396502.html
https://xiaolincoding.com/mysql/transaction/mvcc.html#读提交是如何工作的