脏读只发生在READ UNCOMMITTED隔离级别;MySQL默认REPEATABLE READ通过MVCC在事务启动时创建数据快照,使普通SELECT读取历史版本,从而严格避免脏读,但若ORM或连接池显式设为READ UNCOMMITTED则会失效。

脏读只发生在 READ UNCOMMITTED 级别
结论很直接:只要没显式设成 READ UNCOMMITTED,MySQL 默认的 REPEATABLE READ 就不会脏读。不是靠“运气”,而是 MVCC 在事务启动时就锁定了可见版本范围。
常见错误现象是应用上线后突然出现数据不一致,排查发现 ORM 初始化脚本里有 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED —— 这条命令一执行,MVCC 的快照机制就失效了,后续所有普通 SELECT 都直接读最新行,不管对方提交没提交。
- MySQL 5.7+ 和 8.0 默认都是
REPEATABLE READ,无需额外配置 -
READ COMMITTED同样能防脏读,但每次SELECT都会新建Read View,适合对“实时性”要求略高的场景 - 检查当前会话隔离级别:执行
SELECT @@transaction_isolation,结果必须是REPEATABLE-READ或READ-COMMITTED
MVCC 快照读如何过滤未提交修改
MVCC 不靠锁,靠版本链和 Read View 做可见性判断。当事务 A 修改某行但未提交,事务 B 执行普通 SELECT 时,InnoDB 会沿着该行的 DB_ROLL_PTR 往上找 undo log 版本,再用事务 B 的 Read View 中的 m_ids(活跃事务 ID 列表)比对每个版本的 DB_TRX_ID —— 如果发现该版本由“还在跑”的事务生成,就跳过;只取那些 DB_TRX_ID 小于 min_trx_id 或不在 m_ids 里的版本。
所以事务 B 根本看不到事务 A 那个未提交的修改,连“读到脏数据”的机会都没有。
- 注意:这个机制只对快照读生效,
SELECT ... FOR UPDATE或UPDATE属于当前读,会看到最新已提交数据,并加锁 - 事务启动时机很重要:不是
START TRANSACTION就立刻建Read View,而是第一次执行SELECT(或其它 DML)时才生成 - 如果用
START TRANSACTION WITH CONSISTENT SNAPSHOT,则在命令执行瞬间就建好快照,后续所有快照读都基于它
为什么有些查询还是读到了“脏”值?
真正的问题往往不出在 MVCC 本身,而出在应用层绕过了快照读语义:
- 用了
SELECT ... LOCK IN SHARE MODE或FOR UPDATE却没配事务,导致变成自动提交模式下的当前读,直接读最新行 - 连接池(如 HikariCP)配置了
connectionInitSql,悄悄执行了SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED - ORM(如 MyBatis、Hibernate)开启二级缓存后,缓存命中直接返回旧值,根本没走数据库 —— 这不是脏读,但效果类似
- 误把“主从延迟”当成脏读:从库还没同步完,查从库看到旧数据,但这属于复制问题,和 MVCC 无关
验证 MVCC 是否生效的实操方法
别只看文档,动手确认更可靠。开两个 MySQL 客户端窗口,模拟并发:
窗口 A:BEGIN; UPDATE account SET balance = 100 WHERE id = 1;(不 COMMIT)
窗口 B:SELECT balance FROM account WHERE id = 1; → 返回原值,不是 100
窗口 B 再执行:SELECT @@transaction_isolation; → 确认是 REPEATABLE-READ
窗口 B 再执行:SELECT * FROM information_schema.INNODB_TRX; → 查看当前活跃事务,确认窗口 A 的事务 ID 在 m_ids 里
这一步做完,你就知道 MVCC 正在后台默默工作——它没让你等锁,也没让你读错,只是安静地给你一个“时间切片”。
真正容易被忽略的是:MVCC 防脏读的前提是“用对了读方式”,而很多线上问题,其实卡在连接初始化、ORM 配置或缓存策略这些外围环节上。


















