MySQL事务隔离级别详解:脏读幻读不可重复读现象与实战排查
MySQL事务隔离级别详解:脏读幻读不可重复读现象与实战排查
电商系统中,两个用户几乎同时下单购买最后一件库存,一个订单成功扣减库存,另一个却因为读到了未提交的“脏数据”误以为还有库存 —— 这种经典的并发问题背后,就是事务隔离级别在起作用。本文将用两个终端窗口+完整SQL脚本,手把手演示脏读、不可重复读和幻读现象,并给出生产级的排查思路。
环境准备
本文所有操作基于 MySQL 8.0 ,使用 InnoDB 引擎。你需要打开两个独立的会话(终端1和终端2),方便观察并发行为。
1 | -- 创建测试库与测试表 |
运行环境:MySQL 8.0.32 Community Server,CentOS 7
四种隔离级别速览
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | ✔ | ✔ | ✔ |
| READ COMMITTED | ✘ | ✔ | ✔ |
| REPEATABLE READ | ✘ | ✘ | ✔ (部分) |
| SERIALIZABLE | ✘ | ✘ | ✘ |
- InnoDB 默认隔离级别为 REPEATABLE READ,但通过 MVCC + 间隙锁 很大程度上避免了幻读,只有某些极端场景(如当前读)仍会出现。
- 生产环境一般使用 READ COMMITTED 或 REPEATABLE READ,需结合业务需求。
查看与修改隔离级别
1 | -- 查看全局与当前会话隔离级别 |
脏读(Dirty Read)实战
现象
一个事务读取到另一个事务未提交的数据,当后者回滚后,读到的就是“脏数据”。
复现步骤
目标:事务B在 READ UNCOMMITTED 下读取到事务A尚未提交的余额修改。
1 | -- 终端1(事务A) |
1 | -- 终端2(事务B) |
此时终端1回滚:
1 | -- 终端1 |
终端2再次查询,看到余额变回 1000 ,但之前读到的 500 用于业务判断就会出错。
排查思路
- 通过
SHOW ENGINE INNODB STATUS\G查看事务列表,确认是否有长时间未提交的写事务。 - 检查应用日志中获取余额后是否有校验失败的异常。
解决方案
- 提高隔离级别到 READ COMMITTED 或以上。
- 对关键读取操作加上
SELECT ... FOR UPDATE强制当前读(但会增加锁等待)。
不可重复读(Non-Repeatable Read)实战
现象
同一个事务内,两次读取同一行数据得到不同结果(另一事务在期间提交了修改)。
复现步骤
隔离级别:READ COMMITTED(读已提交),因为 READ UNCOMMITTED 会出现脏读,干扰观察。
1 | -- 终端1(事务A) |
1 | -- 终端2(事务B) |
1 | -- 终端1 继续第二次读取 |
业务场景:事务A需要基于第一次的余额进行复杂计算,却发现金额在中间已被他人修改。
排查思路
- 增加事务开始时间与读取数据时间戳的监控,判断是否出现幻读/不可重复读。
- 使用
SELECT @@session.transaction_isolation确认当前的隔离级别。 - 开启
general_log捕获 SQL 时序(注意性能开销)。
解决方案
- 提升到 REPEATABLE READ,该级别下事务内所有普通 SELECT 看到的是事务开始时的快照,不会出现不可重复读。
- 若必须 READ COMMITTED,可在业务代码中加入乐观锁版本号校验。
幻读(Phantom Read)实战
现象
同一个事务内,两次执行相同条件的查询,结果集的行数发生变化(其他事务插入了满足条件的新行)。
在 InnoDB 的 RR 级别下,快照读(普通 SELECT)由于 MVCC 已消除幻读,但 当前读(SELECT … FOR UPDATE 或 UPDATE/DELETE)仍然可能出现幻读。下面演示后者。
复现步骤:当前读下的幻读
隔离级别:REPEATABLE READ
假设 account 表中存在一些行:
1 | -- 初始数据 |
1 | -- 终端1 |
1 | -- 终端2 |
1 | -- 终端1 再次执行相同 SELECT |
但在 InnoDB 中,FOR UPDATE 会在索引上施加间隙锁(Gap Lock),会阻塞终端2的 INSERT 直至终端1提交。因此默认情况下,RR+间隙锁阻止了幻读。若要真正复现幻读,可以:
- 改用 READ COMMITTED 隔离级别(该级别下无间隙锁)。
- 或者在 RR 下,通过临时的索引失效(如使用函数、类型转换)绕过间隙锁。
这里用 READ COMMITTED 复现:
1 | -- 终端1 |
1 | -- 终端2 |
1 | -- 终端1 再次 FOR UPDATE |
排查实战:锁等待与死锁
幻读引发的最常见问题是锁等待超时或死锁。通过锁监控可以快速定位。
1. 查看当前锁等待
1 | -- 查看 InnoDB 事务信息 |
截取关键部分:
1 | ---TRANSACTION 4219388, ACTIVE 5 sec starting index read |
解读:事务在等待插入意向锁,说明被其他事务的间隙锁阻塞。
2. 死锁日志分析
1 | -- 开启死锁日志打印(默认为开启) |
模拟死锁场景后,SHOW ENGINE INNODB STATUS\G 会输出:
1 | ------------------------ |
从死锁日志可以还原出两个事务互相持有对方需要的锁,从而精确定位有问题的SQL。
解决方案
- 对可能产生幻读的查询,考虑使用唯一索引或乐观锁控制。
- RR 级别下尽量利用 InnoDB 的间隙锁带来的保护,但需注意间隙锁可能造成的插入死锁。
- 若使用 READ COMMITTED,需在应用层进行幂等或重试补偿。
- 通过
pt-query-digest分析慢日志,发现频繁的锁等待。
隔离级别选择建议(生产实战)
1 | 业务特征 推荐隔离级别 原因 |
重要:阿里云 RDS 等云数据库默认隔离级别多为 READ COMMITTED,以适应高并发写入场景,但应用侧必须做好幂等设计。
核心要点
- 脏读:读取未提交数据,唯一解决方案是至少使用 READ COMMITTED。
- 不可重复读:读取已提交数据但同一事务内前后不一致,升级到 REPEATABLE READ 即可解决。
- 幻读:针对当前读(FOR UPDATE/DELETE/UPDATE)的行数变化,InnoDB 通过间隙锁在 RR 级别下大部分消除,但 READ COMMITTED 下仍存在,需要开发者意识。
- 排查三板斧:
SHOW ENGINE INNODB STATUS看锁等待、死锁日志分析、慢查询日志结合performance_schema。 - 隔离级别越高,一致性越强,但并发性能越低。生产环境需在一致性与性能间权衡,并辅以补偿机制。
本文由 Claude(Anthropic)辅助生成。代码示例已在 MySQL 8.0.32 中验证通过。验证日期:2026-08-08。
