行锁竞争等待的根本原因是锁得不准、锁得太久、锁得不稳:WHERE条件未走索引会导致全表扫描并加锁,事务中混入RPC/sleep等操作会延长锁持有时间,隔离级别和加锁顺序不当则引发锁不稳定;EXPLAIN显示ALL/index必须立即优化,ref/range/const才安全。

行锁竞争等待不是“并发太高”导致的,而是锁得不准、锁得太久、锁得不稳——只要 WHERE 条件没走索引,哪怕只更新一行,InnoDB 也可能扫全表并锁住成百上千行。
EXPLAIN 看不到 const/ref 就别上线
这是最直接的判断依据。UPDATE 或 DELETE 执行前必须 EXPLAIN,重点盯 type 字段:
- 出现
ALL或index:说明没走有效索引,正在全表扫描加锁,立刻停手 -
ref、range、const才算安全;rows值应接近实际影响行数,而不是动辄上万 - 联合索引失效常见于跳过最左列(如索引
(a,b,c),但查询只用WHERE b = 1) -
WHERE id = '123'(id是INT)会触发隐式转换,索引失效;必须写成WHERE id = 123
事务里混 RPC、sleep、日志就等于锁住整张表
锁在 COMMIT 才释放,事务每多执行 1ms,其他事务就多等 1ms。真实业务中,锁等待往往来自“非数据库操作”:
- HTTP 调用、MQ 发送、本地文件写入、复杂计算,全得移出事务块
- 不要写
BEGIN; SELECT ...; 处理逻辑; UPDATE ...; COMMIT;,而要BEGIN; SELECT ... FOR UPDATE WHERE id = ?; UPDATE ...; COMMIT; - 批量更新拆成小批:用
WHERE id BETWEEN ? AND ?分页,每次最多 500 行,及时COMMIT - 长事务监控命令:
SELECT * FROM information_schema.INNODB_TRX WHERE trx_state = 'RUNNING' AND TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 30;
READ COMMITTED 不是可选项,是高并发下的默认起点
默认的 REPEATABLE READ 在更新时默认加间隙锁(Gap Lock),哪怕你只改 id = 100,也会锁住 90–110 这个范围,阻塞插入和邻近更新。而 READ COMMITTED 只锁命中的行,且语句执行完就释放不匹配的锁:
- 必须确认 binlog 格式为
ROW(SHOW VARIABLES LIKE 'binlog_format';),否则主从可能不一致 - 全局设置才生效:
SET GLOBAL transaction_isolation = 'READ-COMMITTED';,会话级设置无效 - 业务需接受“不可重复读”:同一事务内两次
SELECT可能返回不同结果,多数订单、支付类场景可接受 - 降级后仍需配合索引优化,否则
READ COMMITTED下照样会因全表扫描锁大量行
SELECT FOR UPDATE 不是保险栓,是放大器
很多人以为加了 SELECT ... FOR UPDATE 就更安全,其实它只是把锁提前显式化——如果 WHERE 条件没走索引,它反而会锁得更重、更广:
- 先
EXPLAIN SELECT ... FOR UPDATE,确认key和rows合理;rows显示 10000?那这把锁已经失控 - 避免
WHERE status = 'pending'这类低基数字段查询,即使有索引,MySQL 也可能放弃使用 - 优先用主键或唯一索引精准定位:
SELECT * FROM orders WHERE order_id = 12345 FOR UPDATE - 多个事务更新同一批记录时,务必统一加锁顺序(如都按
ORDER BY id ASC),否则极易死锁
真正卡住系统的,往往不是锁本身,而是“本不该存在的锁”——比如没索引的 UPDATE、事务里夹着 HTTP 调用、或者默认隔离级别下无意识的间隙锁扩散。优化不是调参,是让每一行锁都精准、短暂、可控。


















