mysql事务中如何避免幻读_mysql隔离机制解析

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

幻读到底是什么,和不可重复读有什么区别

幻读不是“看到幻觉”,而是指事务中两次

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 写法和业务逻辑耦合不深的问题。

相关推荐