第一步必须执行select @@transaction_isolation确认当前会话隔离级别,mysql 5.7/8.0默认为repeatable-read,连接池复用可能导致实际级别与预期不符;若返回repeatable-read且执行范围查询,则next-key lock已激活,这是性能损耗主因而非bug。

查当前会话的 transaction_isolation 是不是第一步
别跳过这步直接改配置。很多“幻读感知”其实根本不是幻读,而是你压根没意识到当前连接用的是 REPEATABLE-READ(MySQL 5.7/8.0 默认),而应用代码里又没显式设隔离级别,连接池复用后还带着旧会话状态。
执行:
SELECT @@transaction_isolation;如果返回
REPEATABLE-READ,且你正在做范围查询(比如 WHERE created_at > '2026-01-01'),那 Next-Key Lock 就已默认激活——它锁住索引间隙,阻塞其他事务插入,这是性能损耗的起点,不是 bug。
SELECT ... FOR UPDATE 在 RR 下是否真有必要
很多人加 FOR UPDATE 是为了“防止幻读”,但没想清楚代价:
• 如果查询条件没走索引,InnoDB 退化为全表扫描 + 行锁,间隙锁失效,FOR UPDATE 只锁现有行,新插入仍能成功,幻读照常发生;
• 如果查询走了索引,FOR UPDATE 会触发 Next-Key Lock,锁住整个范围,其他事务 INSERT 同一范围会被阻塞,表现为 Lock wait timeout exceeded 或长时间等待;
• 更隐蔽的问题:长事务中反复执行带 FOR UPDATE 的相同查询,会持续持有间隙锁,拖慢整个表的写入吞吐。
建议先用 EXPLAIN 确认查询是否命中索引,再决定是否加锁;若只是读取校验,SELECT ... LOCK IN SHARE MODE 开销更小。
监控 INNODB_TRX 和锁等待链
幻读本身不报错,但由它引发的锁竞争会暴露在等待行为里:
• 查活跃事务:
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query FROM information_schema.INNODB_TRX WHERE trx_state = 'LOCK WAIT';重点关注
trx_query 是否含范围条件 + FOR UPDATE 或 LOCK IN SHARE MODE;• 查锁等待关系:
SELECT * FROM performance_schema.data_lock_waits;(需开启
performance_schema)看谁在等谁的间隙锁;• 检查
innodb_row_lock_time_avg 和 innodb_row_lock_waits 状态变量,突增说明间隙锁争抢严重;• 注意:
SHOW ENGINE INNODB STATUS\G 里的 TRANSACTIONS 部分会列出最近死锁和锁等待,但只保留最后一次,不能当长期监控用。READ-COMMITTED 能降级幻读压力,但得接受一致性妥协
把隔离级别降到 READ-COMMITTED,确实能绕过 Next-Key Lock,INSERT 不再被阻塞,QPS 通常会上升。但它解决的是“性能损耗”,不是“幻读问题”本身:
• 每次 SELECT 都新建 MVCC 快照,两次查询之间可能看到新提交的行,结果集行数变化就是幻读;
• 如果业务逻辑依赖“同一事务内多次查询结果稳定”(比如先查库存再扣减),降级后必须靠应用层加分布式锁或重试机制兜底;
• MySQL 8.0+ 全局默认已改为 READ-COMMITTED,但老系统迁移时要注意 ORM 框架(如 Hibernate)可能硬编码了 REPEATABLE-READ;
• 切换前务必压测:观察 Handler_read_next(索引遍历次数)和 Created_tmp_tables(临时表生成量)是否异常升高——RC 下优化器更倾向走全表扫描。
真正难处理的不是“要不要锁”,而是“锁的范围是否精准”。一个没索引的 WHERE status = 'pending' 查询,在 REPEATABLE-READ 下会锁全表间隙,所有 INSERT 都排队;而加个 status 索引,锁就收敛到几个值区间。索引设计比调隔离级别更能治本。











