read committed 下幻读“看起来没发生”是因为每次 select 基于新快照且不锁范围,导致两次查询行数不同但数据库不报错,用户仅感知数据变多;mysql 和 postgresql 均允许幻读,仅保证不读未提交数据。

READ COMMITTED 下幻读为什么“看起来没发生”
因为 READ COMMITTED 每条 SELECT 都用新快照,不锁范围,所以两次查询可能看到不同行数——但数据库(如 PostgreSQL)默认不报幻读错误,用户只觉“数据变多了”,误以为没幻读。MySQL 的 InnoDB 在 READ COMMITTED 下其实也允许幻读,只是普通 SELECT 不加间隙锁,不会阻塞插入,所以现象更隐蔽。
- 典型场景:分页查订单,第一页查完、别人插入新订单、第二页查出重复或跳过某条
- 关键点:
READ COMMITTED不保证多次SELECT结果一致,仅保证不读到未提交数据 - PostgreSQL 实际行为:每次
SELECT基于当前事务启动时刻的最新已提交快照,无范围一致性保障 - MySQL InnoDB 行为:
READ COMMITTED下普通SELECT是快照读,不加next-key lock,插入不受阻,幻读可发生但不报错
REPEATABLE READ 怎么“阻止”幻读(又为什么有时还出现)
REPEATABLE READ 通过首次 SELECT 后复用同一快照 + 间隙锁(MySQL)或谓词锁(PostgreSQL 可串行化才真用)来抑制幻读,但效果因引擎而异。MySQL InnoDB 在该级别下对范围查询自动加 next-key lock,能挡住其他事务在区间内插入;而 PostgreSQL 的 REPEATABLE READ 实际等价于 SQL 标准的 SERIALIZABLE,靠可串行化快照检测冲突,不是靠锁——所以它不阻止插入,而是插入后提交时可能被回滚。
- MySQL 示例:
SELECT * FROM orders WHERE status = 'pending' FOR UPDATE会锁住满足条件的索引区间,新插入 pending 订单会被阻塞 - PostgreSQL 示例:同样语句不加锁,插入成功,但若后续更新触发可串行化冲突,第二个事务会收到
ERROR: could not serialize access due to read/write dependencies among transactions - 注意:
REPEATABLE READ下的普通SELECT(无FOR UPDATE/LOCK IN SHARE MODE)在 MySQL 仍是快照读,不锁任何东西——幻读只在“当前读”场景被控制 - 性能代价:MySQL 的间隙锁可能引发更多锁等待;PostgreSQL 的可串行化检测带来额外校验开销
怎么验证自己遇到的是不是幻读
别只看 SELECT 结果行数变化——得确认是否同一事务内、相同 WHERE 条件、无其他 DML 干扰下,第二次 SELECT 多出了第一次不存在的行,且该行由其他事务插入并已提交。
- 复现步骤:事务 A 执行
SELECT * FROM t WHERE x > 10→ 得到 3 行;事务 B 插入x = 15并COMMIT;事务 A 再执行同样SELECT→ 得到 4 行 - 必须排除干扰:确保事务 A 没执行过
UPDATE/DELETE等当前读操作,否则快照可能提前升级 - MySQL 中,如果用了
SELECT ... LOCK IN SHARE MODE,那第二次执行其实是当前读,看到新行不算幻读,是预期行为 - PostgreSQL 中,
REPEATABLE READ下两次普通SELECT必然返回相同结果(同快照),多出行只可能发生在READ COMMITTED或显式SELECT FOR UPDATE
生产环境该选哪个?别只看理论隔离级别
实际选型取决于业务对“一致性”的容忍粒度和并发写压力。READ COMMITTED 更轻量、冲突少,适合报表类、状态流转不强依赖连续读的场景;REPEATABLE READ 对一致性要求高,但 MySQL 下易因间隙锁导致死锁,PostgreSQL 下则可能因串行化失败迫使应用重试。
- 电商下单:库存校验+扣减需强一致性,
REPEATABLE READ更稳妥,但得配合SELECT ... FOR UPDATE显式加锁 - 后台数据导出:只读、允许轻微延迟,
READ COMMITTED足够,还能减少长事务拖慢其他写入 - 参数差异:
innodb_locks_unsafe_for_binlog已废弃,但老配置可能影响间隙锁行为;PostgreSQL 的default_transaction_isolation设为repeatable read实际启用可串行化 - 容易踩的坑:以为设了
REPEATABLE READ就万事大吉,结果忘记在关键查询加FOR UPDATE,导致 MySQL 仍发生幻读
幻读不是“有没有”,而是“何时暴露、如何拦截”。同一个隔离级别,在不同数据库、不同语句类型(快照读 vs 当前读)、不同索引结构下表现可以完全不同。真正要盯住的,是你的那条 SELECT 到底走的是快照还是加锁路径。










