脏读仅在read uncommitted隔离级别下发生:两会话均执行set session transaction isolation level read uncommitted;会话a begin后update不commit,会话b select即可见未提交值,a rollback后该值消失。

怎么复现脏读:开启两个 mysql 会话,用 READ UNCOMMITTED
脏读只在事务隔离级别为 READ UNCOMMITTED 时可能真实发生。InnoDB 默认是 REPEATABLE READ,所以必须显式改掉——不是改配置文件,而是对当前会话执行:
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
否则哪怕你写了 BEGIN,也看不到脏读。
常见错误现象:SELECT 看到另一事务还没 COMMIT 的修改,但对方随后 ROLLBACK 了,你的查询结果就“凭空多出/少掉”数据。
- 两个会话都要先
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED - 会话 A 执行
BEGIN→UPDATE t SET val = 100 WHERE id = 1(不COMMIT) - 会话 B 此时执行
SELECT * FROM t WHERE id = 1,就能查到val = 100 - 会话 A 接着
ROLLBACK,会话 B 再查一次,值又变回原样
READ UNCOMMITTED 下 SELECT 不加锁,但要注意 MVCC 依然生效
很多人以为 READ UNCOMMITTED 就等于“裸读”,其实不是。InnoDB 仍会走 MVCC 路径,只是跳过了对“未提交版本”的可见性过滤——它会把其他事务未提交的最新行版本也纳入快照范围。
这意味着:SELECT 不会阻塞正在写同一行的事务(没 SELECT ... LOCK IN SHARE MODE 那种锁),但你也无法控制看到哪个“未提交版本”。如果多个事务并发改同一行,你查到的可能是中间某次修改,而它最终不会落地。
- 不加锁 ≠ 不依赖 undo log;MVCC 结构仍在,只是可见性判断逻辑放宽
- 无法预测看到的是谁的未提交变更,尤其在高并发
UPDATE场景下,结果不稳定 -
INSERT ... SELECT或子查询中出现脏读,比单条SELECT更难排查
为什么生产环境几乎不用 READ UNCOMMITTED
不是因为它“慢”,而是它破坏了应用层对数据一致性的基本假设。比如一个订单状态更新流程:A 服务标记“支付中”,B 服务读到后触发发券,结果 A 因异常回滚,“已发券”就成悬空状态。
性能上它确实略快(省了行可见性检查),但代价远超收益:
- ORM 框架(如 mybatis、hibernate)通常默认走
REPEATABLE READ,硬切隔离级别容易和框架缓存、二级缓存冲突 - MySQL 8.0+ 对
READ UNCOMMITTED的优化极少,很多内部路径仍按标准 MVCC 处理 - 监控和慢查日志里看不出脏读痕迹,问题往往暴露在业务对账或用户投诉时
测试时怎么确认真发生了脏读,而不是幻读或不可重复读
关键看三点:是否读到了**未提交**的数据、该数据之后被**回滚**、且读操作发生在**回滚之前**。幻读和不可重复读都要求对方已 COMMIT。
最稳妥的验证方式:在会话 B 的 SELECT 后,立刻查 information_schema.INNODB_TRX,找那个未提交事务的 TRX_ID 和 TRX_STATE = 'RUNNING',再等会话 A ROLLBACK 后对比数据变化。
- 别只靠“两次
SELECT结果不同”下结论——那可能是不可重复读(对方已COMMIT) -
SHOW ENGINE INNODB STATUS里的TRANSACTIONS部分能看见未提交事务,但刷新不及时 - 用
SELECT * FROM performance_schema.data_locks看不到READ UNCOMMITTED的锁信息,因为它确实不加锁
实际测脏读,最难的不是让现象出现,而是确认你看到的“脏”确实是未提交的、且后续被丢弃的——这需要交叉验证事务状态,而不是只盯 SQL 结果。