为什么 SELECT ... FOR UPDATE
会卡住其他事务
根本原因不是索引本身,而是 MySQL 在加锁时是否能精准定位到目标行。没有合适索引时,
SELECT ... FOR UPDATE可能升级为间隙锁(gap lock)甚至全表扫描锁,导致大量无关行被锁定。
比如执行
SELECT * FROM orders WHERE status = 'pending' FOR UPDATE,若
status列无索引,InnoDB 只能遍历聚簇索引逐行判断,过程中对所有扫描过的索引页加临键锁(next-key lock),其他事务更新任意
orders行都可能被阻塞。 优先为
WHERE条件列建索引,尤其是高并发查询+更新的组合字段(如
(status, created_at)) 避免在
WHERE中对索引列做函数操作,如
WHERE DATE(created_at) = '2024-01-01'会让索引失效 用
EXPLAIN确认执行计划是否走了索引,重点关注
type是否为
ref/
range,而非
ALL或
index
唯一索引 vs 普通索引对锁粒度的影响
唯一索引(含主键)能让 InnoDB 精确锁定单一行,而普通二级索引在 RC 隔离级别下仍可能触发间隙锁——尤其当查询条件未命中任何记录时。
例如:表
t(id PK, name VARCHAR(50), idx_name INDEX(name)),执行
SELECT * FROM t WHERE name = 'xxx' FOR UPDATE: 若
name是唯一索引且值存在 → 只锁匹配的那条记录(record lock) 若
name是普通索引且值不存在 → 锁住该值应插入的位置前后间隙(gap lock),阻止其他事务插入同名记录 若
name无索引 → 全表扫描 + 大量临键锁,阻塞面极大
所以,对高频
FOR UPDATE查询的字段,优先建唯一索引;若业务允许,用主键代替非主键条件做锁定更安全。
如何用 SELECT ... LOCK IN SHARE MODE
替代 FOR UPDATE
降低冲突
当业务逻辑只需要“读取后校验再决定是否更新”,而非“必须独占修改”,
LOCK IN SHARE MODE是更轻量的选择——它允许多个事务同时读,仅在真正执行
UPDATE时才升级为排他锁。
典型场景:秒杀扣库存
错误做法:直接SELECT stock FROM items WHERE id = 123 FOR UPDATE→ 长时间持锁,后续请求排队 优化做法:
SELECT stock FROM items WHERE id = 123 LOCK IN SHARE MODE→ 快速读取,校验库存充足后,再执行
UPDATE items SET stock = stock - 1 WHERE id = 123 AND stock >= 1(利用 WHERE 条件做原子校验) 注意:该方案依赖
UPDATE的 WHERE 条件包含足够约束,否则可能超卖
innodb_lock_wait_timeout
不是解药,只是兜底
调大
innodb_lock_wait_timeout(默认 50 秒)只会让等待更久,不解决锁竞争本质。真正有效的是缩短锁持有时间 + 缩小锁范围。 检查长事务:
SELECT * FROM information_schema.INNODB_TRX WHERE TIME_TO_SEC(NOW()) - TIME_TO_SEC(TRX_STARTED) > 10避免在事务里做 HTTP 请求、文件读写等耗时操作 把大事务拆成多个小事务,例如分批更新:用
WHERE id BETWEEN ? AND ?加索引控制范围 监控锁等待:
SHOW ENGINE INNODB STATUS中的
LATEST DETECTED DEADLOCK和
TRANSACTIONS部分最能暴露问题模式
锁等待的本质是资源争用,索引只是让争用更聚焦;一旦业务逻辑要求频繁修改同一行(如计数器),光靠索引优化已不够,得考虑应用层缓存或异步化。
