最准方法是查information_schema.innodb_trx和innodb_lock_waits;若组合查询结果为空则无锁等待,非空可定位阻塞链,其中update/delete走非唯一索引易触发间隙锁。

怎么看当前有没有索引锁等待
直接查 information_schema.INNODB_TRX 和 information_schema.INNODB_LOCK_WAITS 是最准的入口。别只盯着 SHOW PROCESSLIST,它看不到行级锁等待,只能看到语句卡住,但不知道卡在哪一行、被谁锁着。
常用组合查询:
SELECT r.trx_id waiting_trx_id,
r.trx_mysql_thread_id waiting_thread,
r.trx_query waiting_query,
b.trx_id blocking_trx_id,
b.trx_mysql_thread_id blocking_thread,
b.trx_query blocking_query
FROM information_schema.INNODB_TRX r
JOIN information_schema.INNODB_LOCK_WAITS w ON r.trx_id = w.requesting_trx_id
JOIN information_schema.INNODB_TRX b ON b.trx_id = w.blocking_trx_id;
- 如果结果为空,说明当前没有活跃的锁等待;非空则立刻定位到阻塞链
-
waiting_query里出现UPDATE或DELETE且 WHERE 条件命中非唯一索引,大概率是索引间隙锁(gap lock)在作祟 - 注意
blocking_query是否已提交——有时事务没显式COMMIT或崩溃未回滚,会持续持锁
为什么 UPDATE WHERE 非唯一索引会锁一大片
MySQL 在可重复读(RR)隔离级别下,默认用 next-key lock(记录锁 + 间隙锁),目的是防止幻读。但如果你的 WHERE 条件走的是普通二级索引(比如 status 字段没建唯一约束),InnoDB 会锁定满足条件的所有索引项,以及这些索引项之间的间隙——哪怕那些间隙里根本没数据。
- 例如
UPDATE orders SET paid=1 WHERE status='pending',而status是非唯一索引,可能锁住从 'pending' 开始直到下一个索引值(如 'shipped')之间的整个范围 - 如果该索引上 'pending' 值特别多,或分布稀疏(比如只有几条 pending,但索引页跨度大),锁范围会远超预期
-
READ-COMMITTED级别能禁用 gap lock,但代价是可能读到幻读,且不适用于需要强一致性的业务场景
如何快速验证是不是索引设计导致锁竞争
核心思路:看执行计划是否真的走了你认为的索引,并确认该索引是否足够“精确”。
- 对问题 SQL 执行
EXPLAIN FORMAT=TRADITIONAL,重点看key列是否命中预期索引,rows是否远大于实际影响行数 - 用
SELECT * FROM table WHERE ... FOR UPDATE复现同样 WHERE 条件,再查INNODB_LOCKS,观察LOCK_TRX_ID和LOCK_DATA——后者会显示具体锁住的索引值或间隙(如 'pending', 'pending' 表示开区间) - 临时给 WHERE 字段加唯一索引(测试环境!),再跑相同 UPDATE,看锁等待是否消失;若消失,基本坐实是索引区分度低引发的锁放大
线上不敢动索引,有什么临时缓解手段
改索引风险高,但锁竞争又得压。优先从 SQL 和事务控制入手:
- 把大范围 UPDATE 拆成小批量,比如加
LIMIT 100并用 while 循环重试,每次只锁少量记录,降低冲突概率 - 确保事务尽量短:UPDATE 前别做 HTTP 调用、文件读写等耗时操作;避免在事务里嵌套复杂逻辑
- 检查应用层是否用了长连接+自动提交关闭(
autocommit=0),忘了COMMIT就等于长期持锁 - 紧急时可用
KILL掉长时间运行的阻塞事务(先查INNODB_TRX的trx_started时间),但只是治标
真正难处理的是那种“WHERE 条件本身无法优化、又必须高频更新”的场景——这时候锁竞争不是配置或语法问题,而是数据模型和访问模式存在结构性矛盾,得回到业务逻辑重新评估。











