update/delete未走索引会锁表,因innodb行锁加在索引上,全表扫描时每行加锁且repeatable read下触发间隙锁;应确保where命中索引、避免函数操作、分离非db操作、合理分页、慎用select ... for update,并可考虑降级为read committed减少锁范围。

UPDATE/DELETE没走索引会锁表,不是锁行
MySQL的行锁实际加在索引上,不是数据行本身。WHERE条件没命中索引时,InnoDB只能全表扫描,对每条扫描到的记录都加行锁——效果等同于锁表,尤其在REPEATABLE READ下还会触发间隙锁,把整个索引区间都封死。
常见错误现象:SHOW ENGINE INNODB STATUS\G里出现大量*** (1) WAITING FOR THIS LOCK TO BE GRANTED;慢查询日志中Rows_examined高达几万,但业务逻辑只改一行。
- 用
EXPLAIN确认key字段非NULL,且rows值合理(别动不动几万) - 避免在索引列上用函数:
WHERE DATE(create_time) = '2026-04-01'→ 改成WHERE create_time >= '2026-04-01' AND create_time - 避免隐式类型转换:
WHERE user_id = '123'(user_id是INT)会丢索引 - 联合索引注意最左匹配:
INDEX(a, b, c)能用于WHERE a = 1 AND b > 10,但不能用于WHERE b = 10
事务里调HTTP或SLEEP,锁就挂着不放
InnoDB遵循两阶段锁协议:锁在需要时加,但必须等到COMMIT或ROLLBACK才释放。事务里哪怕只SLEEP(2)、调一次支付接口、写一条本地日志,前面所有已加的锁都继续占着——其他事务就在那干等。
典型症状:应用层报Lock wait timeout exceeded; try restarting transaction,但SQL本身很快;监控发现Innodb_row_lock_time_avg持续偏高,且集中在某几条业务路径。
- 把所有非DB操作(HTTP调用、MQ发送、PDF生成、日志打点)全部移出事务块
- 批量更新必须分页,例如
UPDATE t SET status=1 WHERE id BETWEEN ? AND ?,每次控制在100–500行 - 高频热点更新(如库存扣减)考虑拆分,比如
stock_shard_0~stock_shard_7,再聚合 - 若必须“先锁再调外部服务”,改用
SELECT ... FOR UPDATE NOWAIT或WAIT 1,失败立刻返回,不等
READ COMMITTED能砍掉大部分间隙锁
REPEATABLE READ默认加间隙锁(Gap Lock)防幻读,哪怕你只UPDATE一行,也可能锁住整段索引区间。而READ COMMITTED完全不加间隙锁,只锁命中的记录本身,锁范围大幅收窄。
适用场景很明确:业务能接受“不可重复读”(比如后台报表刷新两次看到不同数据)、高频范围更新(UPDATE order SET state=2 WHERE create_time > '2026-04-01')、大量INSERT ... SELECT并发写入。
- 降级前确认
binlog_format = ROW,否则主从可能不一致 - 依赖间隙锁做业务校验的逻辑(如防重复下单)需同步改造,改用唯一索引或应用层幂等
- 可通过
SELECT @@transaction_isolation和SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED动态切换,无需重启 -
RC下普通SELECT仍是快照读,但SELECT ... FOR UPDATE只看到已提交版本,锁也只加在当前匹配行上
SELECT FOR UPDATE不是查数据,是查+加锁+等释放
它本质是“查+加行锁+等锁释放”,高并发下极易形成锁等待链。尤其当查询没走索引、扫描范围大,或事务里混着慢逻辑,持锁时间拉长数秒——其他事务全卡在那条语句上。
真实问题往往藏在看似正常的SQL里:比如开头就SELECT ... FOR UPDATE WHERE status = 'pending',但status没索引,结果锁了全表;或者把它和缓存更新、日志记录包进同一事务,锁白白多挂200ms。
- 务必确认
WHERE条件字段有有效索引,否则可能升级为表锁或间隙锁 - 避免在事务开头就执行
SELECT ... FOR UPDATE,更别把它和业务无关操作包进同一事务 - 如果只是校验后插入(如防重复下单),优先用唯一索引 +
INSERT ... ON DUPLICATE KEY UPDATE,绕过显式锁 - 真需要悲观锁时,确保
SELECT ... FOR UPDATE紧贴后续UPDATE,中间不掺任何耗时操作
锁竞争的复杂点不在SQL写得对不对,而在事务边界是否干净、索引是否真正生效、隔离级别是否被业务误判——这三个地方一碰就慢,而且日志里往往只报超时,不告诉你锁在哪卡住了。











