幻读到底是什么,和不可重复读有什么区别
幻读不是“看到幻觉”,而是指事务中两次
SELECT同一范围数据,第二次多出(或少掉)了新插入(或删除)的行——这些行在第一次查询时并不存在,但被别的事务提交了。关键在于:它针对的是「范围查询」,比如
WHERE age > 25或
SELECT * FROM user WHERE status = 1,而不是单行主键查询。
不可重复读是查同一行,值变了;幻读是查一个范围,行数变了。很多人混淆,是因为 MySQL 默认的
REPEATABLE READ隔离级别下,InnoDB 用间隙锁(Gap Lock)+ 行锁模拟出“无幻读”效果,但这只是表象——它没真正消除幻读语义,只是把并发写冲突提前拦住了。
MySQL 的 REPEATABLE READ 真的能防止幻读吗
在 InnoDB 中,
REPEATABLE READ下普通
SELECT是快照读(Snapshot Read),不加锁,靠 MVCC 版本链避免脏读和不可重复读;但只要涉及当前读(如
SELECT ... FOR UPDATE、
UPDATE、
DELETE),InnoDB 就会加临键锁(Next-Key Lock),即「行锁 + 间隙锁」,从而封锁索引区间,阻止其他事务在该范围内插入新记录。
这意味着:
如果你只用纯SELECT(无锁读),幻读不会发生——因为看到的是事务开始时的一致快照 但如果你执行
SELECT ... FOR UPDATE查范围,然后另一个事务尝试
INSERT进这个间隙,会被阻塞或报死锁——这不是“防止幻读”,而是“用锁压制了幻读发生的条件” 一旦你没走索引(比如
WHERE name LIKE '%abc%'),间隙锁退化为全表锁,性能雪崩
想真正规避幻读,必须用 SERIALIZABLE 吗
不用。MySQL 的
SERIALIZABLE确实会对所有
SELECT自动加上
LOCK IN SHARE MODE,变成串行执行,彻底杜绝幻读,但代价极高:并发能力几乎归零,且容易触发大量锁等待。
更务实的做法是:
确保范围查询字段有有效索引——间隙锁只在索引上生效,否则锁不住间隙 用SELECT ... FOR UPDATE显式加锁时,尽量缩小范围,避免
WHERE条件太宽泛 业务层配合:对“新增是否合法”做幂等校验,比如插入前先
SELECT COUNT(*)判断是否已存在同类逻辑约束,而不是依赖隔离级别兜底 必要时改用应用级分布式锁(如 Redis 锁住业务维度 key),比数据库锁更可控
为什么加了索引还是出现幻读
常见原因不是索引没建,而是用了非唯一索引 + 查询条件未命中索引最左前缀,导致优化器放弃使用间隙锁。例如:
CREATE INDEX idx_status ON user(status); -- 下面这句可能不走间隙锁: SELECT * FROM user WHERE status = 1 AND deleted = 0 FOR UPDATE;
如果
deleted不在索引中,InnoDB 可能只对
status = 1加间隙锁,而允许其他事务在相同
status下插入
deleted = 1的新行——这就是漏掉的“幻行”。
验证方式很简单:
执行EXPLAIN确认是否走了预期索引 查
INFORMATION_SCHEMA.INNODB_TRX和
INNODB_LOCKS(8.0+ 用
performance_schema.data_locks)看实际加了什么锁 避免在
FOR UPDATE查询中混用函数、隐式类型转换、
OR条件——它们都可能导致锁退化
幻读的本质不是数据库“bug”,而是隔离级别与并发控制策略之间的权衡结果;指望靠调高隔离级别一劳永逸,往往掩盖了索引设计、SQL 写法和业务逻辑耦合不深的问题。
