select 会被锁住是因为在rr隔离级别下,带for update、lock in share mode或隐式当前读时会加行锁/间隙锁;若无索引则升级为临键锁。

为什么 SELECT 也会被锁住?
很多人以为只有 UPDATE、delete 才会加锁,其实 InnoDB 在可重复读(RR)隔离级别下,普通 SELECT 是快照读(不加锁),但一旦带上 FOR UPDATE、LOCK IN SHARE MODE,或者隐式升级为当前读(比如在唯一索引上做 UPDATE ... WHERE 前的条件检查),就会触发行锁甚至间隙锁。更隐蔽的是:如果查询走不到索引,可能退化为表级扫描,进而升级为临键锁(Next-Key Lock),锁住一大片范围。
如何快速定位正在阻塞你的查询?
别猜,直接查 information_schema 和 performance_schema:
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 b JOIN information_schema.INNODB_LOCK_WAITS w ON b.trx_id = w.blocking_trx_id JOIN information_schema.INNODB_TRX r ON r.trx_id = w.requesting_trx_id;
配合下面这条看锁详情:
SELECT * FROM performance_schema.data_locks WHERE OBJECT_SCHEMA = 'your_db' AND OBJECT_NAME = 'your_table';
- 注意
LOCK_MODE字段:是RECORD(行锁)、GAP(间隙锁)还是REC_NOT_GAP(仅记录锁) -
LOCK_DATA显示具体锁住的索引值,能帮你判断是不是因为没走索引导致锁范围过大
哪些写法会让 SELECT ... FOR UPDATE 锁得更久?
锁等待时间长,往往不是锁本身重,而是持有锁的事务拖得太久。以下写法极易踩坑:
- 在事务里先执行
SELECT ... FOR UPDATE,然后做耗时操作(如 http 调用、文件读写、复杂计算),再才UPDATE—— 锁从第一行SELECT就开始持有了 - 用
ORDER BY ... LIMIT 1配合FOR UPDATE,但ORDER BY字段没索引,导致全表扫描+全表加锁 - WHERE 条件用了函数或类型隐式转换,例如
WHERE DATE(create_time) = '2024-01-01'或WHERE user_id = '123'(user_id 是 int),导致索引失效,锁范围爆炸 - 批量操作时用
IN列表过长(比如几百个 ID),InnoDB 可能放弃使用索引而走全表扫描
真正有效的优化方向有哪些?
减少锁等待,核心是「缩小时长 + 缩小范围 + 避免冲突」:
- 把
SELECT ... FOR UPDATE尽量靠近后续的UPDATE,中间不要穿插业务逻辑;必要时拆成两个事务:先查,再根据结果决定是否开启新事务加锁更新 - 确保所有
FOR UPDATE查询都走索引——用EXPLAIN看type是否为ref/range,key是否命中预期索引 - 高并发扣减类场景(如库存),考虑用乐观锁替代:去掉
FOR UPDATE,改用UPDATE ... SET stock = stock - 1 WHERE id = ? AND stock >= 1,靠影响行数判断是否成功 - 确认是否真需要 RR 隔离级别;若业务能接受读已提交(RC),则间隙锁禁用,大幅降低锁冲突概率(但需评估幻读风险)
最常被忽略的一点:锁等待超时默认是 50 秒(innodb_lock_wait_timeout),但很多应用层没设 query timeout,导致一个慢查询卡住整个线程池。务必在客户端连接配置里加上合理超时,而不是依赖 MySQL 默认值。