脏读仅发生在READ UNCOMMITTED级别下,MySQL默认REPEATABLE READ可避免脏读但存在幻读;需通过SERIALIZABLE或SELECT...FOR UPDATE彻底解决幻读,ORM和连接池可能静默修改隔离级别引发隐患。

脏读只发生在 READ UNCOMMITTED 隔离级别下
MySQL 默认隔离级别是 REPEATABLE READ,这个级别下不会发生脏读。只有显式把事务设为 READ UNCOMMITTED,才可能读到其他事务尚未提交的修改。
常见错误现象:应用中执行了 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED,又没意识到后续所有 SELECT 都可能读到未提交数据;或者 ORM(如 Django)配置了全局低隔离级别,导致业务查询意外读取到回滚前的中间状态。
- 使用场景极少:一般只用于对一致性无要求的统计类查询(如实时在线人数估算),且必须接受“可能读到根本不存在的数据”
-
READ UNCOMMITTED下,SELECT不加锁,也不检查行版本,直接读最新写入的记录 - 一旦另一个事务在你
SELECT后立刻ROLLBACK,你就拿到了逻辑上从未成功存在的值
如何复现一次典型的脏读
需要两个并发会话,手动控制事务节奏。关键点在于:Session B 在 Session A 提交前就读取了其修改。
-- Session A START TRANSACTION; UPDATE accounts SET balance = 1000 WHERE id = 1; -- 不执行 COMMIT 或 ROLLBACK,保持事务开启-- Session B(已设为 READ UNCOMMITTED) SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; START TRANSACTION; SELECT balance FROM accounts WHERE id = 1; -- 返回 1000,即脏读 -- 此时若 Session A 执行 ROLLBACK,则该 1000 从未真正生效
为什么 REPEATABLE READ 能避免脏读但仍有幻读
REPEATABLE READ 使用多版本并发控制(MVCC),每个事务启动时拍一个快照,后续所有 SELECT 都基于这个快照——它天然过滤掉未提交事务的变更,所以不可能读到脏数据。
但它不阻止其他事务插入新行并提交,因此同一范围的 SELECT 可能两次返回不同数量的行(幻读)。这不是脏读,因为插入的行是已提交的合法数据。
- 脏读本质是“读到了不该存在的数据”,幻读本质是“读到了新出现的、合法的数据”
- 想彻底避免幻读,需升级到
SERIALIZABLE(加范围锁)或用SELECT ... FOR UPDATE显式锁定区间 - 注意:
innodb_locks_unsafe_for_binlog=ON(已弃用)曾让 RR 行为退化,现代 MySQL 无需担心
ORM 和连接池常悄悄改变隔离级别
Django 的 ATOMIC_REQUESTS、Spring 的 @Transactional(isolation = Isolation.READ_UNCOMMITTED)、或是某些数据库连接池(如 HikariCP)预设的 connection-init-sql,都可能覆盖会话默认隔离级别。
排查时不要只看代码里的 SQL,要确认实际执行前的会话状态:
SELECT @@transaction_isolation, @@session.transaction_isolation;
- 连接池可能复用连接,而上一个使用者改过隔离级别,导致当前请求“继承”了异常设置
- Go 的
database/sql包中,db.Exec("SET SESSION...")只影响单次调用,但若用tx, _ := db.Begin(),则需在tx上显式Exec - PHP PDO 中,
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, ...)不影响隔离级别,但$pdo->exec("SET SESSION ...")会影响后续所有语句
实际线上环境几乎从不主动启用 READ UNCOMMITTED,真正容易被忽略的是:隔离级别被某处配置静默修改后,脏读成为偶发、难复现的数据异常根源。










