脏读指事务读取到其他未提交事务修改的数据;READ UNCOMMITTED 允许直接读取未提交变更,若对方回滚则导致数据污染,如报表导出包含幻影订单。

什么是脏读,以及为什么 READ UNCOMMITTED 会触发它
脏读指一个事务读取了另一个未提交事务修改过的数据。只要隔离级别设为 READ UNCOMMITTED,MySQL 就不加限制地允许这种读取——哪怕另一事务随后 ROLLBACK,读到的数据也早已“污染”了当前逻辑。
典型场景:后台导出报表时,用 SELECT * FROM orders 查询订单表,而此时前端正发起一笔新订单但尚未提交(只执行了 INSERT INTO orders),导出脚本就可能把这笔“幻影订单”算进去,后续对账直接出错。
实操建议:
- 线上业务库禁止使用
READ UNCOMMITTED,连测试环境都应规避 -
SELECT操作默认走REPEATABLE READ(MySQL 8.0+ 默认)或READ COMMITTED,已天然屏蔽脏读 - 若需临时观察未提交变更(如 DBA 排查),改用
SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE显式加锁,而非降级隔离级别
READ COMMITTED 怎么保证读不到未提交数据
READ COMMITTED 的核心机制是:每次 SELECT 都生成一个新的快照(基于当前已提交的事务 ID 列表),因此只能看到在语句开始前已 COMMIT 的数据。这比 REPEATABLE READ 的“事务级快照”更激进刷新,但足以拦截脏读。
示例对比:
-- 会话 A
START TRANSACTION;
INSERT INTO users (name) VALUES ('alice');
-- 此时未 COMMIT-- 会话 B(隔离级别为 READ COMMITTED) START TRANSACTION; SELECT * FROM users WHERE name = 'alice'; -- 返回空 COMMIT;
关键点:
- 会话 B 的
SELECT不会阻塞,也不读到 A 的未提交行 - 如果会话 B 的隔离级别是
REPEATABLE READ,结果一样——它同样不脏读,只是后续相同SELECT仍返回空(复用首次快照) - 真正差异体现在“不可重复读”:若 A 在 B 两次
SELECT之间COMMIT,READ COMMITTED会读到新值,REPEATABLE READ不会
REPEATABLE READ 下为何仍可能“看似脏读”
MySQL 的 REPEATABLE READ 默认使用 MVCC + Next-Key Lock,理论上杜绝脏读。但开发者常误判的“脏读”,其实来自三类非隔离级别问题:
- 应用层缓存未失效:ORM(如 MyBatis)二级缓存或 Redis 缓存了旧数据,和数据库隔离级别无关
- 显式跳过事务:代码中用了
set autocommit = 1或START TRANSACTION后忘记COMMIT,导致后续SELECT实际在 autocommit 模式下执行(即每条语句自成事务) - 跨连接读取:事务 A 在连接 1 中修改未提交,事务 B 在连接 2 中查询——此时 B 看不到 A 的修改,但如果 B 用的是长连接且之前执行过
SELECT,可能复用旧快照,造成“读到了又没读到”的困惑
验证方法:在会话内执行 SELECT @@transaction_isolation; 确认当前级别;用 SHOW ENGINE INNODB STATUS\G 查看锁等待,排除锁冲突误判。
生产环境该选哪个级别来稳住脏读底线
绝大多数 OLTP 场景,REPEATABLE READ 是 MySQL 的安全基线——它不脏读、不不可重复读(同一事务内多次读一致)、还支持间隙锁防幻读。只有两类情况需主动切到 READ COMMITTED:
- 高并发更新热点行,
REPEATABLE READ的 Next-Key Lock 可能导致锁范围过大,升级为READ COMMITTED后退化为行锁(需配合innodb_locks_unsafe_for_binlog=OFF,但 MySQL 8.0 已移除该参数) - 需要与 Oracle/PostgreSQL 对齐行为,或使用逻辑复制(binlog 格式为
ROW时,READ COMMITTED更易预测)
真正容易被忽略的是:即使级别设对了,如果事务里混用 DDL(如 ALTER TABLE)、或调用 SELECT ... FOR UPDATE 但未覆盖索引,锁机制失效,照样可能引发一致性异常——隔离级别管的是“读可见性”,不是“写安全性”。










