幻读到底是什么现象?
幻读不是数据“变脏”或“被改”,而是同一个事务里,两次执行一模一样的范围查询(比如
SELECT * FROM orders WHERE status = 'pending'),第二次却多出了一行——这行是别的事务在两次查询之间
INSERT进来的。它像幽灵一样“凭空出现”,所以叫“幻读”。关键点:不是同一行被改了(那是不可重复读),而是**行数变了**。
为什么可重复读(RR)隔离级别下还会幻读?
MySQL 默认的
REPEATABLE READ确实靠 MVCC 实现快照读,能挡住其他事务的
UPDATE/DELETE对已存在行的影响,但对新插入的行无感——因为新行的
trx_id比当前事务启动时的
snapshot版本号还大,MVCC 不会把它纳入快照。所以:
• 普通
SELECT(快照读):看不到新插入的行 → 表面没幻读
• 但一旦你执行
SELECT ... FOR UPDATE或
INSERT ... SELECT这类当前读,InnoDB 就必须查真实索引结构 → 新行就暴露了 → 幻读立刻发生
怎么真正拦住幻读?三个实操手段
1. 加 Next-Key Lock(最常用)
• 在范围条件上加锁,比如
SELECT * FROM t WHERE id > 100 FOR UPDATE,InnoDB 不仅锁住已有记录,还会锁住
(100, +∞)这个间隙
• 要求字段有索引(主键或二级索引),否则退化为表锁
2. 升级到
SERIALIZABLE
• MySQL 会自动把所有普通
SELECT改成
SELECT ... LOCK IN SHARE MODE
• 并发急剧下降,线上慎用
3. 应用层配合(推荐高频场景)
• 比如“先查再插”逻辑,不用
SELECT ... INSERT,改用
INSERT IGNORE或
ON DUPLICATE KEY UPDATE
• 避免依赖两次查询的一致性,直接靠唯一约束兜底
最容易被忽略的坑
•
SELECT * FROM t WHERE name = 'xxx'—— 如果
name没索引,InnoDB 无法加间隙锁,幻读照常发生
•
UPDATE语句只锁匹配到的行,不锁间隙;如果想防幻读,得显式加
FOR UPDATE或改写为带范围条件的锁读
•
READ COMMITTED下连 MVCC 快照都每条语句刷新一次,幻读比 RR 更频繁,别误以为“读已提交更安全”
