MySQL 事务隔离级别详解(JavaGuide 转载)
转载自 JavaGuide 原文,作者:SnailClimb 与 guang19。
依据 Apache-2.0 许可证收录,见固定快照。
本站移除推广与外部图片嵌入、调整技术链接,技术勘误在文内标注;配图位置的“本站补充”由 IT枫斗者编写,并非原图;文中“我”指原作者。
本文由 SnailClimb 和 guang19 共同完成。
关于事务基本概览的介绍,请看这篇文章的介绍:MySQL 常见知识点&面试题总结(原文参考)
事务隔离级别总结
SQL 标准定义了四种事务隔离级别,用来平衡事务的隔离性(Isolation)和并发性能。级别越高,数据一致性越好,但并发性能可能越低。这四个级别是:
- READ-UNCOMMITTED(读取未提交) :最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。这种级别在实际应用中很少使用,因为它对数据一致性的保证太弱。
- READ-COMMITTED(读取已提交) :允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。这是大多数数据库(如 Oracle, SQL Server)的默认隔离级别。
- REPEATABLE-READ(可重复读) :对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。MySQL InnoDB 存储引擎的默认隔离级别正是 REPEATABLE READ。并且,InnoDB 在此级别下通过 MVCC(多版本并发控制) 和 Next-Key Locks(间隙锁+行锁) 机制,在很大程度上解决了幻读问题。
- SERIALIZABLE(可串行化) :最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
| 隔离级别 | 脏读 (Dirty Read) | 不可重复读 (Non-Repeatable Read) | 幻读 (Phantom Read) |
|---|---|---|---|
| READ UNCOMMITTED | √ | √ | √ |
| READ COMMITTED | × | √ | √ |
| REPEATABLE READ | × | × | √ (标准) / ≈× (InnoDB) |
| SERIALIZABLE | × | × | × |
默认级别查询:
MySQL InnoDB 存储引擎的默认隔离级别是 REPEATABLE READ。可以通过以下命令查看:
- MySQL 8.0 之前:
SELECT @@tx_isolation; - MySQL 8.0 及之后:
SELECT @@transaction_isolation;
mysql> SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ |
+-------------------------+InnoDB 的 REPEATABLE READ 对幻读的处理:
标准的 SQL 隔离级别定义里,REPEATABLE READ 是无法防止幻读的。但 InnoDB 的实现通过以下机制很大程度上避免了幻读:
- 快照读 (Snapshot Read):在 InnoDB RR 下,普通一致性 SELECT 通常在本事务首次一致性读时建立 Read View,后续一致性读复用它;仅执行 BEGIN 不等于已建立快照。START TRANSACTION WITH CONSISTENT SNAPSHOT 可在 RR 下显式建立一致性快照。事务可以看到自身修改,因此应区分自身写入与其他事务提交的可见性。
- 加锁读与写操作:RR 下的范围扫描通常使用 next-key 锁保护索引记录及相关间隙,但不能将所有 INSERT、UPDATE、DELETE 和加锁读统一视为同一种锁。完整唯一键等值命中通常只锁记录;未命中时可能涉及间隙。INSERT 通常先取得插入意向间隙锁,再对插入记录加锁;唯一键和外键检查另有规则。RC 通常减少搜索与索引扫描中的间隙锁,实际锁范围仍取决于隔离级别、索引访问路径和语句条件。
值得注意的是,虽然通常认为隔离级别越高、并发性越差,但 InnoDB 存储引擎通过 MVCC 机制优化了 REPEATABLE READ 级别。对于许多常见的只读或读多写少的场景,其性能与 READ COMMITTED 相比可能没有显著差异。不过,在写密集型且并发冲突较高的场景下,RR 的间隙锁机制可能会比 RC 带来更多的锁等待。
本站版本说明:MySQL 8.4 手册说明,对读现象敏感的分布式事务可优先考虑 SERIALIZABLE,RR 可能不足;不能把下面历史书籍引述中的“必须”概括为当前 XA 接口仅允许这一种隔离级别。XA 原子提交与读隔离应分别判断。依据 MySQL 8.4 XA 说明。以下保留原文书籍引述。
《MySQL 技术内幕:InnoDB 存储引擎(第 2 版)》7.7 章这样写到:
InnoDB 存储引擎提供了对 XA 事务的支持,并通过 XA 事务来支持分布式事务的实现。分布式事务指的是允许多个独立的事务资源(transactional resources)参与到一个全局的事务中。事务资源通常是关系型数据库系统,但也可以是其他类型的资源。全局事务要求在其中的所有参与的事务要么都提交,要么都回滚,这对于事务原有的 ACID 要求又有了提高。另外,在使用分布式事务时,InnoDB 存储引擎的事务隔离级别必须设置为 SERIALIZABLE。
实际情况演示
在下面我会使用 2 个命令行 MySQL ,模拟多线程(多事务)对同一份数据的脏读问题。
MySQL 命令行的默认配置中事务都是自动提交的,即执行 SQL 语句后就会马上执行 COMMIT 操作。如果要显式地开启一个事务需要使用命令:START TRANSACTION。
我们可以通过下面的命令来设置隔离级别。
SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE]我们再来看一下我们在下面实际操作中使用到的一些并发控制语句:
START TRANSACTION|BEGIN:显式地开启一个事务。COMMIT:提交事务,使得对数据库做的所有修改成为永久性。ROLLBACK:回滚会结束用户的事务,并撤销正在进行的所有未提交的修改。
脏读(读未提交)
本站图解(非原图)
读未提交下的脏读。未查看原图;以下表名、字段和值均为本站自设教学示例,不还原截图。本文补充按 MySQL 8.4 / InnoDB 说明。
两个独立连接 A、B;每轮示例均从已提交的 account(id=1, balance=100) 开始。A 在事务外先执行 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;。普通查询为 SELECT balance FROM account WHERE id=1;。
A 读到 B 尚未提交、最终又撤销的值,这就是脏读;这里的 200 是示意值,不是实测输出。
官方依据:MySQL 事务隔离级别。
避免脏读(读已提交)
本站图解(非原图)
读已提交避免脏读。沿用自设账户与双连接约定,重新从 balance=100 开始;A 在 BEGIN 前设置 READ COMMITTED。每行是严格按顺序执行的一步,A 的读均为普通 SELECT。
| 步骤 | 连接 A | 连接 B |
|---|---|---|
| 1 | BEGIN;首次读取 100 | — |
| 2 | — | BEGIN;UPDATE 改为 200,尚未提交 |
| 3 | 再读仍为 100,不读到未提交的 200 | — |
| 4 | — | COMMIT |
| 5 | 再读为 200;COMMIT | — |
这是 RC 普通一致性读的预期行为:每条读语句取得新的快照,因此 B 提交前后的可见值可以不同。A 不需要等 B 提交才能读到旧的已提交版本。
官方依据:MySQL 一致性非锁定读。
不可重复读
还是刚才上面的读已提交的图,虽然避免了读未提交,但是却出现了,一个事务还没有结束,就发生了 不可重复读问题。
本站图解(非原图)
不可重复读复用上一处 RC 时序表,不重复绘制大图。
| 对比项目 | 第一次普通读 | B 提交后的普通读 |
|---|---|---|
| A 的事务 | 同一个事务,尚未结束 | 仍为该事务 |
| 查询目标 | id=1 的 balance | 相同记录、相同字段 |
| 自设示例值 | 100 | 200 |
两次读之间 B 已提交更新,因此没有脏读,但 A 读到同一行的不同值,构成不可重复读。这里讨论的是同一行值的变化;新增匹配行造成的结果集变化见后面的幻读说明。
官方依据:MySQL 一致性非锁定读:RC 每次读取新快照。
可重复读
本站图解(非原图)
可重复读下的普通一致性读。仍采用自设账户约定,重置 balance=100;A 在 BEGIN 前设置 REPEATABLE READ,A 自己不修改数据。
| 步骤 | 连接 A | 连接 B |
|---|---|---|
| 1 | BEGIN;首次普通 SELECT 读到 100,建立快照 | — |
| 2 | — | BEGIN;UPDATE 改为 200;COMMIT |
| 3 | 同一事务再次普通 SELECT,仍为 100 | — |
| 4 | COMMIT;开启新事务后普通 SELECT 为 200;COMMIT | — |
通常是首次一致性读建立快照,并非执行 BEGIN 就自动建立;START TRANSACTION WITH CONSISTENT SNAPSHOT 可显式请求一致性快照。这张表只讨论普通 SELECT:锁定读和写操作的可见性不能直接套用此表;同一事务自己的先前修改也会被自己读到。
官方依据:MySQL 一致性非锁定读。
幻读
演示幻读出现的情况
本站图解(非原图)
RR 中混用快照读与当前读的时序。以下是为对应正文“工资 500”情境构造的示例,并非原图的精确 SQL 或执行结果。另设 InnoDB 表 employee(id INT PRIMARY KEY, salary INT, KEY idx_salary(salary)),初始仅 (1,500) 一行;两连接事务外均设为 RR,A 无自身写入。
可核对的 A 查询分别为 SELECT id FROM employee WHERE salary=500 ORDER BY id; 与 SELECT id FROM employee WHERE salary=500 ORDER BY id FOR UPDATE;。B 插入语句为 INSERT INTO employee VALUES (2,500);。
| 比较方式 | 解释 |
|---|---|
| 同一 RR 事务的两次普通 SELECT | 使用同一快照,不看到 B 后插入的行 |
| 普通 SELECT 与之后的 FOR UPDATE | 读语义不同,后者可看到 B 已提交的新行;不能据此断言 RR 快照读出现幻读 |
| 若 A 从第一次查询就使用 FOR UPDATE | 对该非唯一条件的索引范围加锁可阻止匹配范围中的并发插入,B 需等待相关锁释放 |
Next-key lock 结合索引记录锁与其前方间隙锁;实际锁范围受索引和扫描范围影响,不等于锁住整张表。唯一索引唯一条件命中记录时可仅锁该记录,不能把所有当前读都概括为必然加间隙锁。
官方依据:MySQL 隔离级别:锁定读与范围锁、一致性非锁定读、Phantom Rows 与 next-key locking。
SQL 脚本 1 在第一次查询工资为 500 的记录时只有一条,SQL 脚本 2 插入了一条工资为 500 的记录,提交之后;SQL 脚本 1 在同一个事务中再次使用当前读查询发现出现了两条工资为 500 的记录这种就是幻读。
注:这个例子本质上是第一次快照读和第二次当前读的读语义不同。RR 下,MVCC 可以保证快照读不出现幻读,Next-Key Lock 可以约束当前读;但同一事务内混用快照读和当前读时,两次读取看到的结果可能不同。
解决幻读的方法
解决幻读的方式有很多,但是它们的核心思想就是一个事务在操作某张表数据的时候,另外一个事务不允许新增或者删除这张表中的数据了。解决幻读的方式主要有以下几种:
- 将事务隔离级别调整为
SERIALIZABLE。 - 在可重复读的事务级别下,给事务操作的这张表添加表锁。
- 在可重复读的事务级别下,给事务操作的这张表添加
Next-key Lock(Record Lock+Gap Lock)。
参考
- 《MySQL 技术内幕:InnoDB 存储引擎》
- https://dev.MySQL.com/doc/refman/5.7/en/
- Mysql 锁:灵魂七拷问
- Innodb 中的事务隔离级别和锁的关系
