根本原因是无索引字段导致行锁退化为临键锁甚至表锁;应通过EXPLAIN确认执行计划类型,为高频查询列建联合索引,并避免在索引列上使用函数。

为什么 SELECT ... FOR UPDATE 会卡住其他事务
根本原因不是锁本身,而是被锁的行没有走索引——MySQL 在无索引字段上执行行锁时,会退化为临键锁(Next-Key Lock),锁住整个范围,甚至可能升级成表级锁。常见于 WHERE status = 'pending' 这类查询,如果 status 没有索引,哪怕只更新 1 行,也可能堵住全表写入。
实操建议:
- 用
EXPLAIN看执行计划,确认type是ref或range,而不是ALL或index - 对
WHERE、ORDER BY、JOIN中高频出现的列,优先建联合索引,顺序按「等值查询 → 最左前缀 → 范围查询」排,例如(user_id, status, created_at) - 避免在索引列上做函数操作,
WHERE DATE(created_at) = '2024-01-01'会让索引失效,改用WHERE created_at >= '2024-01-01' AND created_at
如何快速定位正在争用的行锁
靠 SHOW ENGINE INNODB STATUS\G 太难读,真正有用的是 information_schema.INNODB_TRX 和 INNODB_LOCK_WAITS 的组合查法。它能直接告诉你谁在等、等谁、等哪一行。
实操建议:
- 执行
SELECT * FROM information_schema.INNODB_TRX WHERE trx_state = 'LOCK WAIT';找出阻塞中的事务 - 关联
INNODB_LOCK_WAITS查blocking_trx_id,再反查INNODB_TRX得到持锁事务的trx_mysql_thread_id - 用
SELECT * FROM performance_schema.threads WHERE THREAD_ID = ?定位对应线程的 SQL(需开启performance_schema) - 注意:
trx_query字段可能为空或被截断,真实语句要从应用日志或慢查日志里交叉验证
UPDATE 语句没走索引却锁了整张表
这不是 bug,是 MySQL 的乐观假设:当优化器预估扫描行数超过一定比例(通常约 20%),它会放弃使用索引,改走主键聚簇索引全扫——而 UPDATE 在这种情况下会对所有扫描过的主键记录加 X 锁,相当于锁表效果。
实操建议:
- 检查
EXPLAIN FORMAT=JSON中的filtered值,低于 10 就高度可疑;再看rows是否远超实际匹配数 - 用
ANALYZE TABLE更新统计信息,有时旧的统计会导致误判 - 强制走索引仅作临时排查:
UPDATE t USE INDEX (idx_status) SET ... WHERE status = 'done',但别长期依赖 hint - 如果业务确实需要低区分度字段(如
status只有 3 个值),考虑用冗余字段 + 条件索引,比如加is_pending TINYINT并建索引,值由应用维护
高并发下 INSERT ... ON DUPLICATE KEY UPDATE 的锁行为
这个语句看似原子,其实分三步:先查唯一键、再判断冲突、最后插入或更新。中间任何一步都可能被其他事务打断,导致死锁或间隙锁膨胀。尤其在批量插入场景,很容易触发 Lock wait timeout exceeded。
实操建议:
- 确保冲突检测字段(如
UNIQUE KEY (order_no))有唯一索引,否则会降级为普通索引,间隙锁范围扩大 - 批量插入时,按主键或唯一键升序排序后再执行,能显著降低死锁概率
- 避免在同一个事务里混用
INSERT ... ON DUPLICATE KEY UPDATE和普通UPDATE,尤其是更新同一张表不同条件的行 - 如果只是防重复插入,且业务允许少量失败,用
INSERT IGNORE更轻量,它不加 S 锁,只加插入意向锁
最常被忽略的一点:间隙锁(Gap Lock)不是“锁住了某条数据”,而是“锁住了某个不存在的空档”。你查不到它,也杀不掉它,只能靠索引设计和查询收敛来规避。一旦出现抖动,先看是不是在没索引的字段上做了范围操作。











