脏读仅发生在read uncommitted级别,因其跳过一致性检查、不生成readview、不查undo log,直接读缓冲池中未提交的最新行记录;其他级别均通过readview或锁机制杜绝脏读。

脏读只发生在隔离级别设为 READ UNCOMMITTED 时,其他级别下不会出现——这不是 SELECT 本身的问题,而是你主动允许它读未提交数据。
为什么 SELECT 会读到未提交的修改
根本原因在于数据库跳过了所有一致性检查:不生成 ReadView、不查 undo log、不比对事务 ID。它直接从缓冲池(buffer pool)里拿最新写入的行记录,哪怕那条记录所属的事务还在运行、甚至最终会回滚。
常见触发场景包括:
- 应用连接初始化时执行了
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED,后续所有SELECT都默认走这个级别 - MySQL 客户端工具(如某些老版本 MySQL Workbench 或命令行)在非交互模式下默认使用该级别
- Django 的
ATOMIC_REQUESTS=True配合中间件未显式指定隔离级别,而底层 DB 连接池复用了被污染的会话设置
READ COMMITTED 下还能脏读吗
不能。这是它和 READ UNCOMMITTED 的硬分界线。
READ COMMITTED 要求每次 SELECT 都基于当前已提交的版本构建 ReadView,自动过滤掉所有未提交事务的修改。即使另一个事务正在执行 UPDATE 但卡在 COMMIT 前,你的 SELECT 也完全感知不到那行被动过。
注意例外:
-
SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE是“当前读”,它们不依赖 ReadView,而是直接读最新已提交版本 + 加锁,但仍不会读到未提交数据 - 如果应用层手动开了事务但忘了
COMMIT或ROLLBACK,且另一连接用READ UNCOMMITTED去查,就稳稳命中脏读——问题不在 SQL 写法,而在事务生命周期管理失控
MySQL 默认隔离级别为什么没救你
MySQL InnoDB 默认是 REPEATABLE READ,按理说不会脏读。但现实里仍出问题,往往因为:
- ORM 框架(如旧版 MyBatis 或某些 JDBC 封装)在获取连接后,悄悄执行了
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED来“提升查询性能” - 数据库管理员改过全局变量:
SET GLOBAL tx_isolation = 'READ-UNCOMMITTED',导致所有新连接继承该设置 - 你用的是代理层(如 ProxySQL、MaxScale),它把客户端声明的隔离级别给覆盖或降级了
验证当前会话真实级别,别信文档或配置文件,直接执行:SELECT @@tx_isolation 或 SELECT @@transaction_isolation。
最容易被忽略的调试盲区
不是所有“看到未提交值”的情况都叫脏读。比如:
- 你在事务 A 中
UPDATE t SET x=1 WHERE id=1,还没COMMIT;同时事务 B 执行SELECT x FROM t WHERE id=1 FOR UPDATE—— 这时事务 B 会阻塞,直到事务 A 结束,它读到的是 A 提交后的值(或回滚后的原值),这属于正常锁等待,不是脏读 - 应用用了连接池,某个连接在上一次使用中被设成了
READ UNCOMMITTED,但没重置,下次被复用时就静默带毒
真正要盯住的,永远是会话级隔离级别的实际取值,而不是你“以为”它该是什么。










