能解决超卖问题,但必须在事务中用SELECT...FOR UPDATE且WHERE条件命中索引,否则升级表锁;事务外执行锁秒释放,无保护作用;索引失效导致全表锁定;死锁需按主键升序加锁并重试;事务应尽量短,锁等待超时宜设为3–5秒。

直接结论:能解决,但必须在事务中用 SELECT ... FOR UPDATE,且 WHERE 条件必须命中索引,否则会升级为表锁,反而拖垮整个库。
为什么 SELECT ... FOR UPDATE 必须放在事务里?
MySQL 的行锁(包括 FOR UPDATE)只在显式事务中生效。如果 autocommit=1(默认),每条语句自动提交,SELECT ... FOR UPDATE 执行完立刻释放锁,后续的 UPDATE 就完全不受保护——等于没锁。
常见错误现象:
- 单独执行
SELECT stock FROM products WHERE id = 1 FOR UPDATE;,不加BEGIN或START TRANSACTION,锁秒放 - MyBatis 中 mapper 方法没加
@Transactional,导致 Spring 不开启事务 - 事务中途抛异常但没回滚,连接被复用,锁残留(尤其连接池场景)
WHERE 条件没走索引,会锁整张表
InnoDB 的行锁依赖索引。如果 SELECT ... FOR UPDATE 的 WHERE 条件无法使用索引(比如对非索引字段查询、函数包装、隐式类型转换),InnoDB 会退化为表级锁。
后果很直接:
- 所有对该表的写操作(
INSERT/UPDATE/DELETE)全部阻塞 - 其他无关商品的扣减也被卡住,QPS 断崖下跌
- 查
SHOW ENGINE INNODB STATUS\G会看到大量TABLE LOCK记录
验证方式:EXPLAIN SELECT stock FROM products WHERE id = 1 FOR UPDATE; 看 key 字段是否非 NULL,type 是否为 const 或 ref。
死锁不是小概率事件,而是设计缺陷的信号
两个事务按不同顺序加锁,就可能死锁。例如:
- 事务 A 先锁
id=1,再锁id=2 - 事务 B 先锁
id=2,再锁id=1
MySQL 会自动检测并回滚其中一个事务,报错:Deadlock found when trying to get lock; try restarting transaction。
避免方法:
- 所有业务路径统一按主键 ID 升序加锁(比如先查小 ID,再查大 ID)
- 单次扣减只锁一行(别在同一个事务里
FOR UPDATE多个商品) - 应用层捕获死锁异常后重试,但重试次数必须限制(避免雪崩)
性能瓶颈不在 SQL,而在连接等待和事务时长
SELECT ... FOR UPDATE 本身很快,慢的是“等锁”。高并发下,大量请求卡在 Waiting for table metadata lock 或 Locked 状态,连接池迅速耗尽。
关键控制点:
- 事务越短越好:查库存 → 判断 → 更新 → 提交,中间别掺杂 HTTP 调用、日志打印、复杂计算
- 连接池最大连接数要预留 buffer,否则新请求直接被拒绝(如 HikariCP 的
maximumPoolSize) - 设置合理的锁等待超时:
innodb_lock_wait_timeout(默认 50 秒),建议调低到 3–5 秒,让失败更快暴露
真正容易被忽略的,是事务边界和索引覆盖的双重校验——没做这两件事,FOR UPDATE 不是防超卖,是在给数据库埋雷。


















