脏读、不可重复读、幻读分别对应事务隔离级别的三类问题:脏读在READ UNCOMMITTED下发生,READ COMMITTED及以上可避免;不可重复读在READ COMMITTED下出现,REPEATABLE READ通过MVCC解决;幻读在REPEATABLE READ下仍可能影响当前读,SERIALIZABLE才能完全避免。

脏读:读到了别人还没提交的修改
事务 A 修改了一行数据但没提交,事务 B 此时读到了这行“脏”数据。一旦 A 回滚,B 读到的就是根本不存在的状态。
-
READ UNCOMMITTED隔离级别下必然发生;READ COMMITTED及以上可避免 - 典型场景:报表系统误把临时草稿当真实数据统计
- 错误现象不是报错,而是业务逻辑出错——比如用户看到余额变多了,实际扣款却失败了
- MySQL 默认是
REPEATABLE READ,所以默认不会脏读,但显式设成READ UNCOMMITTED就会
不可重复读:同一事务里两次 SELECT 结果不一致
事务 A 第一次读某行,事务 B 提交了对这行的 UPDATE 或 DELETE,A 再读一次,发现数据变了——这不是幻觉,是真实更新被看到了。
-
READ COMMITTED级别下会发生;REPEATABLE READ(MySQL 默认)用 MVCC 快照避免了它 - 注意:这里改的是“已有行”,不是新增或删除行——那是幻读的范畴
- 示例:
SELECT balance FROM accounts WHERE id = 1;执行两次,中间被别人UPDATE accounts SET balance = 100 WHERE id = 1;并提交,第二次结果就不同 - 性能影响:MVCC 在
REPEATABLE READ下需维护多个版本,undo log 增长快,长事务会拖慢 purge 线程
幻读:查不到的记录突然“冒出来”
事务 A 按条件查了一批记录(比如 SELECT * FROM orders WHERE status = 'pending';),事务 B 插入一条满足该条件的新记录并提交,A 再次执行相同查询,发现多了一条“幻影”记录。
- MySQL 的
REPEATABLE READ用间隙锁(gap lock)+ 行锁 + Next-Key Lock,在大多数情况下阻止幻读——但仅限于当前读(如SELECT ... FOR UPDATE或UPDATE带条件) - 纯快照读(普通
SELECT)在REPEATABLE READ下不会看到新插入的行,所以“感觉不到”幻读;但如果你做UPDATE ... WHERE status = 'pending',可能意外更新到 B 刚插入的那条,这就是幻读的实际影响 - 真正能完全避免幻读的只有
SERIALIZABLE,但它会让并发写变成排队,吞吐暴跌 - 常见坑:开发以为加了事务就万事大吉,结果在
REPEATABLE READ下做“先查后插”的业务逻辑(如防重单),没加SELECT ... FOR UPDATE,导致两条一模一样的订单入库
怎么选隔离级别?别只看理论
MySQL 默认 REPEATABLE READ 是有代价的:间隙锁可能引发死锁,长事务让 undo log 膨胀,备份时 mysqldump --single-transaction 依赖它但也会被阻塞。
- 如果业务能接受“每次读都看到最新已提交数据”,且没有范围更新需求,
READ COMMITTED更轻量,还减少锁冲突 - 高并发写+范围条件更新(比如批量关单、改状态),务必确认是否用了
SELECT ... FOR UPDATE加锁,否则幻读隐患藏在逻辑深处 -
SERIALIZABLE几乎不用在 OLTP 场景——它会让所有SELECT都隐式加锁,连简单查询都可能互相等待 - 最常被忽略的一点:应用层重试逻辑和隔离级别要匹配。比如在
READ COMMITTED下重试 SELECT+UPDATE,可能比REPEATABLE READ下更安全;反过来,若假设“同一事务内数据不变”,却用了READ COMMITTED,就会出错










