脏读的根本原因是事务未提交且隔离级别为read uncommitted:此时select跳过锁与版本检查,直接读取update产生的未提交中间值。

UPDATE 与 SELECT 并发执行时出现脏读,根本原因不是语句本身,而是事务未提交 + 隔离级别设为 READ UNCOMMITTED 或根本没有事务控制。
为什么 SELECT 能读到 UPDATE 中未提交的数据?
脏读只在极低隔离级别下发生:当一个事务执行了 UPDATE 但尚未 COMMIT,另一个事务的 SELECT 就去读同一行,并且它的事务隔离级别是 READ UNCOMMITTED —— 这时数据库明确允许它跳过锁、跳过版本检查,直接读内存或缓冲区里的“最新值”,哪怕这个值属于未提交的中间状态。
常见错误场景:
- 开发时手动开启事务但忘记
COMMIT或ROLLBACK,然后用另一连接去查,看到“诡异”的值 - 应用配置了全局
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED,却误以为只是“查得快” - ORM 框架(如某些老版本 Django 或 MyBatis)默认没设隔离级别,底层驱动用了数据库实例的默认值(MySQL 默认是
REPEATABLE READ,但 PostgreSQL / SQL Server 默认是READ COMMITTED;如果 DBA 改过全局变量,就可能出问题)
READ COMMITTED 下还会脏读吗?
不会。这是它和 READ UNCOMMITTED 的核心分界线。
READ COMMITTED 要求 SELECT 只能看到已 COMMIT 的数据。InnoDB 用的是“语句级快照”:每条 SELECT 执行时,会基于当前已提交的版本构建一个 ReadView,过滤掉所有未提交事务的修改。所以即使 UPDATE 正在运行中,只要没 COMMIT,SELECT 就完全感知不到那行被改过。
注意例外:
- 显式加锁的
SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE会阻塞,但不读脏数据;它等的是锁释放,不是等提交 - 某些数据库(如早期 SQL Server)在
READ COMMITTED下用锁实现,而非 MVCC,这时虽然不脏读,但可能因锁等待导致超时或死锁
MySQL 默认的 REPEATABLE READ 真的能杜绝脏读?
能。而且不只是杜绝脏读,还顺带解决了不可重复读 —— 它用的是“事务级快照”:第一次 SELECT 时生成 ReadView,后续所有 SELECT 都复用它,不管其他事务是否已提交。
但要注意两个现实坑点:
- 快照只对普通
SELECT生效;SELECT ... FOR UPDATE会触发当前读,看到最新已提交版本(可能比快照新),这容易让人误以为“快照失效” - 如果你在同一个事务里先
SELECT,再等别人UPDATE并COMMIT,然后再SELECT ... FOR UPDATE,第二次查到的就是新值 —— 这不是脏读,是当前读的正常行为 - 幻读仍可能发生(比如别人
INSERT了一条新记录并COMMIT),REPEATABLE READ用间隙锁(gap lock)缓解,但不是绝对阻止
如何快速验证是否发生了脏读?
最直接的办法是人工复现 + 查 INFORMATION_SCHEMA.INNODB_TRX:
开两个终端,分别连同一 MySQL 实例:
-- 终端 A(开启事务,UPDATE 但不提交) BEGIN; UPDATE accounts SET balance = 900 WHERE id = 1;
-- 终端 B(查当前活跃事务) SELECT trx_id, trx_state, trx_started, trx_isolation_level FROM INFORMATION_SCHEMA.INNODB_TRX;
如果此时在终端 B 执行 SELECT * FROM accounts WHERE id = 1; 得到了 900,而终端 A 还没 COMMIT,并且你确认终端 B 的隔离级别是 READ UNCOMMITTED(可通过 SELECT @@tx_isolation; 验证),那就是脏读实锤。
真正难排查的,往往是应用层隐式设置了低隔离级别,或者连接池复用导致会话变量污染 —— 这种时候单看 SQL 语句看不出问题,得盯住会话级配置。










