MySQL事务机制详解
2024年1月1日
MySQL事务机制详解
MySQL事务是数据库操作的基本单位,能够保证数据的一致性和完整性。本文详细介绍事务的ACID特性、隔离级别以及实现原理。
1. 事务的ACID特性
- 原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
- 一致性(Consistency):是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。
- 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。
- 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
1.1 InnoDB 如何保证 ACID
- A:undo log(回滚日志)
- I:MVCC(多版本并发控制)
- D:redo log(重做日志)
- C:A+I+D
2. 并行事务引发的问题
MySQL 支持多客户端连接,可能出现同时处理多个事务的情况。以下是并行事务可能引发的问题,按严重性排序:

2.1 脏读 (Dirty Read)
一个事务读到了另一个已修改的数据但是未提交事务,就会发生脏读。
或者说,读到了过期数据。
2.2 不可重复读 (Non-repeatable Read)
在一个事务内,多次读取同一个数据,出现了前后两次读到的数据不一样的情况,就发生了不可重复读。
2.3 幻读 (Phantom Read)
在一个事务内,多次查询某个条件下的记录数量,如果出现了前后两次查询的记录数量不一样,就发生了幻读。
2.3.1 幻读的另一种解释
假设此时平台要升级,用户表中的性别字段,原本是以「男、女」的形式保存数据,现在平台升级后要求改为「0、1」代替。
- 事务A开始更改表中所有数据的性别字段,当负责执行事务A的线程正在更改最后一条表数据时
- **此时事务B来了,**正好向用户表中插入了一条「性别=男」的数据并提交了
- 然后事务A改完原本的最后一条数据后,当再次去查询用户表时,结果会发现表中依旧还存在一条「性别=男」的数据,似乎又产生了幻觉一样
3. 事务隔离级别
3.1 四种隔离级别
- RU:read uncommitted(读未提交)
- RC:read committed(读已提交)
- RR:repeatable read(可重复读)
- Serializable(串行化)

3.2 MySQL 默认 RR 是否解决幻读
回答:并不是完全解决了
- 针对 快照读(普通 select 语句),是 通过 MVCC 方式解决了幻读
- 因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
- 针对 当前读(select ... for update 等语句),是 通过 next-key lock(记录锁+间隙锁)方式解决了幻读
- 因为当执行 select ... for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题
3.3 四种隔离级别的实现方式
- **「读未提交」:**因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好
- **「串行化」:**通过加读写锁的方式来避免并行访问
- 「读提交」和「可重复读」: 通过 Read View 来实现的,区别在于创建 Read View 的时机不同
- 「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View
- 「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View
3.4 开启事务命令的区别
两种事务开启命令:
- begin/start transaction
- start transaction with consistent snapshot
开启时机:
- 执行了 begin/start transaction 命令后,并不代表事务启动了。 只有在执行这个命令后,执行了第一条 select 语句,才是事务真正启动的时机;
- 执行了 start transaction with consistent snapshot 命令,就会马上启动事务。
4. MVCC 机制
通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)
4.1 Read View 的四个字段

- creator_trx_id:代表创建当前这个ReadView的事务ID。
- m_ids:表示在生成当前ReadView时,系统内活跃且未提交的事务 ID列表。
- min_trx_id:活跃的事务列表中,最小的事务ID。
- max_trx_id:表示在生成当前ReadView时,系统中要给下一个事务分配的ID值
4.2 聚集索引记录中和事务有关的两个隐藏列
- 隐藏列不止两个,以下是与事务相关的两个隐藏列:
- trx_id:当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里;
- roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。

4.3 事务访问数据的可见性判断
判定方法 --- 事务 read view 里的字段与记录中的两个隐藏列进行对比:
- 如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值,表示这个版本的记录是在创建 Read View 前 已经提交的事务生成的,所以该版本的记录对当前事务 可见。
- 如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建 Read View 后 才启动的事务生成的,所以该版本的记录对当前事务 不可见。
- 如果记录的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中:
- 如果记录的 trx_id 在 m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务 不可见。
- 如果记录的 trx_id 不在 m_ids 列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务 可见。
5. 可重复读与幻读问题
5.1 可重复读的工作原理
可重复读隔离级别是:启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View
5.2 可重复读是否完全解决幻读
虽然是 MySQL 默认隔离级别,但是没有完全解决幻读
5.2.1 幻读场景1

- 事务 A 执行查询 id = 5 的记录,此时表中是没有该记录的,所以查询不出来。
- 事务 B 插入一条 id = 5 的记录,并且提交了事务。
- 事务 A 更新 id = 5 这条记录,对没错,事务 A 看不到 id = 5 这条记录,但是他去更新了这条记录,这场景确实很违和,然后再次查询 id = 5 的记录,事务 A 就能看到事务 B 插入的纪录了,幻读就是发生在这种违和的场景。
解释:
- 在可重复读隔离级别下,事务 A 第一次执行普通的 select 语句时生成了一个 ReadView,之后事务 B 向表中新插入了一条 id = 5 的记录并提交。
- 接着,事务 A 对 id = 5 这条记录进行了更新操作,在这个时刻,这条新记录的 trx_id 隐藏列的值就变成了事务 A 的事务 id,之后事务 A 再使用普通 select 语句去查询这条记录时就可以看到这条记录了,于是就发生了幻读。
因为这种特殊现象的存在,所以我们认为 MySQL Innodb 中的 MVCC 并不能完全避免幻读现象
5.2.2 幻读场景2
- T1 时刻:事务 A 先执行「快照读语句」:select * from t_test where id > 100 得到了 3 条记录。
- T2 时刻:事务 B 往插入一个 id= 200 的记录并提交;
- T3 时刻:事务 A 再执行「当前读语句」 select * from t_test where id > 100 for update 就会得到 4 条记录,此时也发生了幻读现象。
5.3 解决幻读的方法
要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select ... for update 这类当前读的语句,会触发 Next-Key 锁
6. 读提交隔离级别的工作原理
读提交隔离级别是:在每次读取数据时,都会生成一个新的 Read View