MySQL 事务隔离级别:从脏读到串行化,一篇讲透
面试官:“MySQL 的事务隔离级别有哪些?默认是什么?”
你:“四个级别:读未提交、读已提交(RC)、可重复读(RR,MySQL 默认)、串行化。它们分别解决脏读、不可重复读、幻读问题。”
面试官:“那 RR 级别下如何解决幻读?和 RC 有什么区别?”
你:“……”
很多人能背出级别名称,但一追问“MVCC 怎么实现”“间隙锁是什么”就含糊了。本文从理论基础到 InnoDB 实现,彻底讲透事务隔离级别。
一、事务的四大特性(ACID)
在讲隔离级别之前,先回顾事务的基本特性:
| 特性 | 含义 |
|---|---|
| 原子性(Atomicity) | 事务中的操作要么全部成功,要么全部失败 |
| 一致性(Consistency) | 事务前后数据状态保持一致 |
| 隔离性(Isolation) | 多个事务并发执行时,相互隔离不干扰 |
| 持久性(Durability) | 事务提交后,数据永久保存 |
隔离级别就是用来控制隔离性的强弱程度。隔离级别越高,数据一致性越好,但并发性能越低。
二、三个并发问题
在并发事务中,可能出现以下三种读问题:
| 问题 | 含义 | 示例 |
|---|---|---|
| 脏读(Dirty Read) | 读到另一个事务未提交的数据 | 事务A修改了数据但未提交,事务B读取到,然后事务A回滚,B读到的是脏数据 |
| 不可重复读(Non-Repeatable Read) | 同一事务内两次读取同一条记录,结果不同(因为其他事务更新了该记录) | 事务A第一次读取某行值为10,事务B修改为20并提交,事务A再次读取变成20 |
| 幻读(Phantom Read) | 同一事务内两次查询结果集的行数不同(因为其他事务插入了新行) | 事务A查询id>10的记录有3条,事务B插入一条id=11并提交,事务A再次查询变成4条 |
注意区分:
- 不可重复读:针对同一条记录的修改。
- 幻读:针对一批记录的插入或删除(行数变化)。
三、四种隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 并发性能 |
|---|---|---|---|---|
| 读未提交(Read Uncommitted) | 可能 | 可能 | 可能 | 最高 |
| 读已提交(Read Committed) | 不可能 | 可能 | 可能 | 较高 |
| 可重复读(Repeatable Read,默认) | 不可能 | 不可能 | 理论上可能(InnoDB通过间隙锁解决) | 中等 |
| 串行化(Serializable) | 不可能 | 不可能 | 不可能 | 最低 |
MySQL InnoDB 默认使用可重复读(RR),并且通过MVCC + 间隙锁解决了幻读问题(后面细说)。
四、各隔离级别详解
1. 读未提交(Read Uncommitted)
事务可以读取其他事务未提交的修改。这是最低级别,几乎从不使用。
问题:脏读。
示例:
-- 事务ASTARTTRANSACTION;UPDATEaccountSETbalance=balance-100WHEREid=1;-- 未提交-- 事务B(隔离级别读未提交)STARTTRANSACTION;SELECTbalanceFROMaccountWHEREid=1;-- 读到已扣减但未提交的值-- 如果事务A回滚,事务B读到的就是脏数据适用场景:无,除非你能接受脏数据。
2. 读已提交(Read Committed,RC)
事务只能读取其他事务已经提交的修改。这是很多数据库(如 PostgreSQL、Oracle)的默认级别。
解决的问题:脏读。
仍存在的问题:不可重复读、幻读。
示例(不可重复读):
-- 事务A(RC级别)STARTTRANSACTION;SELECTbalanceFROMaccountWHEREid=1;-- 100-- 此时事务B修改并提交-- 事务A再次查询SELECTbalanceFROMaccountWHEREid=1;-- 可能变成90,不可重复读InnoDB 实现:通过MVCC(多版本并发控制),每次SELECT都会重新生成一个一致性视图(read view),所以能看到已提交的最新数据,因此不可重复读无法避免。
3. 可重复读(Repeatable Read,RR)
MySQL InnoDB 的默认级别。保证在同一事务内,多次读取同一条记录的结果一致。
解决的问题:脏读、不可重复读。
幻读问题:理论上幻读仍可能发生,但 InnoDB 通过间隙锁(Gap Lock)或MVCC在大多数场景下避免了幻读。
示例(可重复读实现):
-- 事务A(RR级别)STARTTRANSACTION;SELECTbalanceFROMaccountWHEREid=1;-- 100-- 事务B修改并提交UPDATEaccountSETbalance=90WHEREid=1;COMMIT;-- 事务A再次查询SELECTbalanceFROMaccountWHEREid=1;-- 仍然是100(可重复读)InnoDB 实现原理:
- 事务第一次
SELECT时,生成一个read view(包含未提交事务的 ID 列表)。 - 后续
SELECT使用同一个 read view,因此看不到其他事务提交的修改,实现了可重复读。
幻读的解决:
对于快照读(普通SELECT),MVCC 天然避免了幻读(因为快照不变)。
对于当前读(SELECT ... FOR UPDATE/UPDATE/DELETE),InnoDB 使用间隙锁锁住查询范围内的间隙,防止其他事务插入新行。
-- 事务ASELECT*FROMtWHEREid>10FORUPDATE;-- 事务B想插入 id=11,会被间隙锁阻塞,从而避免幻读4. 串行化(Serializable)
所有事务串行执行,读操作也会加共享锁(读锁),写操作加排他锁。性能极低,几乎只在数据一致性要求极高且并发极低的场景使用。
解决的问题:脏读、不可重复读、幻读全部解决。
缺点:并发能力几乎为零,容易出现锁超时和死锁。
五、MVCC 机制浅析
MVCC(Multi-Version Concurrency Control)是 InnoDB 实现高并发隔离级别的核心。
原理:
- 每行记录隐藏两个字段:
DB_TRX_ID(最后修改该行的事务ID)、DB_ROLL_PTR(指向 undo log 中旧版本记录的指针)。 - 事务开始时,生成一个read view,包含当前活跃事务的 ID 列表(未提交的)。
- 读取数据时,根据 read view 判断哪个版本可见:只看到事务 ID 小于当前事务且不在活跃列表中的版本。
不同隔离级别下的 read view 生成时机:
- RC:每条
SELECT语句都生成一个新的 read view,所以能看到已提交的最新数据。 - RR:第一条
SELECT生成 read view,整个事务共用同一个,因此可重复读。
六、间隙锁(Gap Lock)与幻读
在 RR 级别下,InnoDB 通过间隙锁来防止幻读。
间隙锁:锁住索引记录之间的“间隙”,防止其他事务在间隙中插入新记录。
示例:
-- 表 t 有 id 列(主键),已有记录 id=10, 20-- 事务ASELECT*FROMtWHEREidBETWEEN10AND20FORUPDATE;-- 此时 InnoDB 不仅锁住 id=10 和 20 的记录,还锁住 (10,20) 之间的间隙-- 事务B 想插入 id=15,会被阻塞,直到事务A提交注意:
- 如果查询使用唯一索引等值查询且记录存在,则只锁记录,不锁间隙(不会产生幻读风险)。
- 如果查询使用普通索引或范围查询,会锁间隙。
- 串行化级别也会加间隙锁,但还会对读操作加共享锁。
七、如何查看和设置隔离级别?
-- 查看当前会话隔离级别SELECT@@transaction_isolation;-- MySQL 8.0SELECT@@tx_isolation;-- MySQL 5.7-- 查看全局隔离级别SELECT@@global.transaction_isolation;-- 设置会话隔离级别SETSESSIONTRANSACTIONISOLATIONLEVELREADCOMMITTED;-- 设置全局隔离级别(需要重启或重新连接生效)SETGLOBALTRANSACTIONISOLATIONLEVELREPEATABLEREAD;八、常见面试追问
Q1:MySQL 默认隔离级别是 RR,为什么很多互联网公司改成 RC?
- RC 的锁机制更简单(没有间隙锁),并发性能更高,尤其在主从复制中,RC + binlog_format=ROW 可以避免间隙锁带来的死锁问题。
- RC 允许不可重复读,但在大多数业务场景(如订单、支付)中,单条查询通常不需要可重复读,通过业务逻辑或乐观锁即可保证一致性。
- 但要注意:RC 下可能出现不可重复读和幻读,需要业务层面接受或使用其他手段。
Q2:RR 级别下能完全避免幻读吗?
普通快照读(不加锁的SELECT)不会出现幻读,因为快照不变。
当前读(SELECT ... FOR UPDATE/UPDATE/DELETE)通过间隙锁避免了幻读。
但有一种情况例外:事务中先快照读,再当前读,可能会看到不同的行数(因为当前读会读取最新提交的数据)。但这不属于严格意义上的幻读(幻读定义是同一查询两次结果集不同,但这里查询方式不同)。一般面试中认为 RR 级别下 InnoDB 解决了幻读。
Q3:MVCC 和锁的关系?
- MVCC 用于快照读(普通
SELECT),实现无锁并发。 - 锁(行锁、间隙锁)用于当前读(
SELECT ... FOR UPDATE、UPDATE、DELETE),保证写操作的正确性。
Q4:如何模拟幻读?
在 RC 级别下,很容易模拟幻读:
-- 事务A(RC)STARTTRANSACTION;SELECT*FROMtWHEREid>10;-- 假设返回2行-- 事务B插入 id=11 并提交INSERTINTOtVALUES(11);COMMIT;-- 事务A再次查询SELECT*FROMtWHEREid>10;-- 返回3行,幻读在 RR 级别下,如果使用当前读,间隙锁会阻塞插入,模拟需要一些技巧(如先快照读,再当前读)。
Q5:隔离级别对性能的影响?
- 读未提交:无锁,性能最高但数据不可靠。
- 读已提交:锁范围小,性能较好。
- 可重复读:加间隙锁,可能导致更多锁冲突,性能中等。
- 串行化:大量锁等待,性能最差。
九、总结
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现方式(InnoDB) |
|---|---|---|---|---|
| 读未提交 | 可能 | 可能 | 可能 | 无锁,直接读最新版本 |
| 读已提交 | 不可能 | 可能 | 可能 | 每条SELECT生成read view |
| 可重复读 | 不可能 | 不可能 | 通常不可能(间隙锁) | 第一条SELECT生成read view,当前读加间隙锁 |
| 串行化 | 不可能 | 不可能 | 不可能 | 读加共享锁,写加排他锁,事务串行 |
一句话记住隔离级别:读未提交脏数据,读已提交不脏但幻不可重;可重复读默认防幻,串行化性能最差。
理解事务隔离级别是数据库优化和面试的基础。希望这篇文章能帮你彻底掌握 MySQL 事务隔离的核心知识,欢迎继续讨论。