mysql幻读是怎么出现的_mysql并发问题分析

来源:这里教程网 时间:2026-02-28 20:45:42 作者:

幻读只在“当前读”下真实发生

很多人误以为可重复读(RR)隔离级别下普通

SELECT
也会幻读,其实不会——RR 下的普通查询是快照读,读的是事务启动时的 MVCC 快照,新插入的行根本不可见。真正出现幻读的,一定是用了
SELECT ... FOR UPDATE
SELECT ... LOCK IN SHARE MODE
UPDATE
/
DELETE
这类“当前读”语句。

典型现象:事务 A 执行

SELECT * FROM orders WHERE status = 'pending' FOR UPDATE
得到 5 行;事务 B 插入一条
status = 'pending'
的新订单并提交;事务 A 再次执行相同语句,突然返回 6 行——多出来的那条就是“幻行”。

为什么 RR 隔离级别仍挡不住幻读

关键在于 InnoDB 的加锁机制:RR 级别下,

WHERE
条件无索引或索引不生效时,InnoDB 只能走全表扫描,此时仅对**实际命中的记录**加行锁(Record Lock),而对未命中但落在查询范围内的“间隙”(Gap)不加锁——这就给其他事务留下了插入空间。

如果
status
字段没建索引,
WHERE status = 'pending'
会锁住所有扫描到的行,但间隙不锁 → 幻读可发生
如果
status
有索引,且查询能走索引,InnoDB 会升级为
Next-Key Lock
(行锁 + 间隙锁),覆盖等值查询的前后间隙 → 大概率阻止幻读
但如果查询条件是
WHERE status > 'a'
这类范围查询,即使有索引,也可能只锁部分间隙,仍存在漏插风险

真正有效的解决方式不是调高隔离级别

SERIALIZABLE
能彻底消灭幻读,但代价是所有
SELECT
都隐式加锁,高并发下极易锁等待甚至死锁,线上基本不用。

更务实的做法是结合场景主动控制:

对关键业务逻辑(如库存扣减、订单生成),在事务开头就用
SELECT ... FOR UPDATE
锁住整个范围,且确保该字段有合适索引
插入前先做一次“存在性校验查询”,但必须搭配
FOR UPDATE
,否则校验和插入之间仍有窗口
用唯一约束(
UNIQUE KEY
)替代应用层判断,让数据库直接拦截重复插入(注意:这防的是重复,不防幻读本身)
业务上接受“最终一致性”的,可改用乐观锁(如版本号字段 +
WHERE version = ?
)配合重试

最容易被忽略的坑:d=5 这种无索引条件

看这个经典例子:

SELECT * FROM t WHERE d = 5 FOR UPDATE
,而
d
字段没有索引。InnoDB 只会对实际满足
d = 5
的行(比如 id=5)加行锁,其余扫描过的行(id=0,10,15…)不加锁,间隙更不锁。这时别人就能在 id=1、id=2 等任意空隙插入
d = 5
的新行,下次当前读就看到幻行了。

这不是 MySQL 的 bug,而是设计取舍:MVCC 解决快照读一致性,锁机制解决当前读一致性,两者分工明确。想靠 RR 一招鲜解决所有并发问题,本身就是对隔离级别的误解。

相关推荐