select for update超时根本原因是锁未及时释放,而非语句执行慢;锁自执行起持续到事务结束,中间混入http调用、日志写入或无索引扫描等操作会延长持锁时间,叠加rr隔离级别下的间隙锁,导致等待超时。

SELECT FOR UPDATE 为什么一用就超时?根本不是它慢,是锁没及时放
超时错误 Lock wait timeout exceeded; try restarting transaction 不是 SELECT FOR UPDATE 执行太久,而是它加的锁一直挂着,别的事务排队等太久,超过了 innodb_lock_wait_timeout(默认 50 秒)。锁从 SELECT FOR UPDATE 执行那一刻起就加上了,直到整个事务 COMMIT 或 ROLLBACK 才释放——中间哪怕只做一次 HTTP 调用、日志写入或循环计算,锁都在死守。
常见表现:
- SHOW PROCESSLIST 看到状态是
Updating或Locked,且Time列持续上涨 - 监控显示
innodb_row_lock_time_avg突然飙升到几百毫秒以上 - 错误日志里反复出现 error 1206,但单条 SQL 的 EXPLAIN 显示走索引、扫描行数极少
哪些写法会让锁“赖着不走”?
事务边界没切好,是锁超时的第一推手。以下操作看似合理,实则让锁持有时间翻倍甚至失控:
- 在
BEGIN后立刻SELECT id FROM order WHERE status = 'pending' FOR UPDATE,接着在应用层遍历结果集、调第三方接口、生成 PDF,最后才UPDATE—— 这期间所有被查到的行都锁着 - 事务里混了
INSERT INTO log_table或file_put_contents()这类非数据库操作 - 用了
ORDER BY created_at LIMIT 1 FOR UPDATE,但created_at没索引,导致全表扫描 + 全表加 X 锁 - WHERE 条件隐式类型转换,比如
WHERE user_id = '123'(user_id是BIGINT),索引失效,锁范围退化为整张聚簇索引
隔离级别怎么悄悄放大锁问题?
MySQL 默认 REPEATABLE READ 隔离级别下,SELECT FOR UPDATE 不只加行锁,还会自动加间隙锁(Gap Lock)和临键锁(Next-Key Lock)。这意味着:
-
SELECT * FROM orders WHERE amount > 100 FOR UPDATE会锁住所有amount > 100的现有行,还锁住“大于 100 的所有空隙”,新插入的订单直接被堵住 - 即使你只查一行,比如
WHERE id = 100,只要id是主键且等值查询,就只加记录锁(Record Lock);但换成WHERE status = 'paid'(status无索引),立马升级为全表锁 - 业务允许“不可重复读”的场景(如后台统计、对账),开头加
SET TRANSACTION ISOLATION LEVEL READ COMMITTED可禁用间隙锁,锁粒度更细、释放更早
怎么快速定位谁在死扛锁不放?
别靠猜。三步定位空闲但未提交的事务:
- 查正在卡住 DDL 或其他查询的阻塞源:
SELECT * FROM information_schema.PROCESSLIST WHERE STATE = 'Waiting for table metadata lock' - 确认谁拿了 S MDL 不放:
SELECT b.ID, b.USER, b.HOST, b.DB, b.INFO FROM information_schema.PROCESSLIST b JOIN information_schema.PROCESSLIST a ON b.ID = a.ID WHERE b.COMMAND != 'Sleep' AND b.TIME > 30 - 看具体锁类型和范围:
SELECT LOCK_TRX_ID, LOCK_MODE, LOCK_TYPE, LOCK_DATA FROM performance_schema.data_locks WHERE OBJECT_SCHEMA = 'your_db' AND OBJECT_NAME = 'your_table',重点看LOCK_MODE是RECORD还是GAP
最隐蔽的坑是:事务已经空闲(状态为 Sleep),但没显式 COMMIT 或 ROLLBACK,S MDL 和行锁还在。这种“僵尸事务”往往来自异常未捕获、连接池配置不当或 PHP/Java 中忘记手动结束事务。











