能解决超卖,但必须在显式事务中执行、WHERE条件命中索引、事务生命周期可控,否则锁无效甚至拖垮数据库。

直接结论:SELECT ... FOR UPDATE 能解决超卖,但必须在显式事务中执行、WHERE 条件必须命中索引、事务生命周期必须可控,否则锁无效甚至拖垮数据库。
为什么单独写 SELECT ... FOR UPDATE 没用?
MySQL 的行锁只在显式事务中持续生效。如果 autocommit=1(默认),语句执行完立刻提交,锁秒释放——后续的 UPDATE 完全裸奔。
- 常见错误:在命令行或测试脚本里直接执行
SELECT stock FROM products WHERE id = 123 FOR UPDATE,没加BEGIN或START TRANSACTION - ORM 场景:MyBatis 方法没加
@Transactional,Spring 不开启事务,FOR UPDATE形同虚设 - 连接池陷阱:事务异常未回滚,连接被复用,残留锁可能阻塞后续请求
WHERE 条件不走索引会锁整张表
InnoDB 行锁依赖索引。一旦 WHERE 字段无索引、用了函数、发生隐式类型转换,InnoDB 就会退化为表锁。
- 危险写法:
SELECT stock FROM products WHERE sku = 'ABC' FOR UPDATE(sku未建索引) - 更危险:
SELECT stock FROM products WHERE CAST(id AS CHAR) = '123' FOR UPDATE(函数导致索引失效) - 验证方式:执行
EXPLAIN SELECT stock FROM products WHERE id = 123 FOR UPDATE,检查key是否非NULL、type是否为const或ref
死锁不是偶然,是顺序没对齐
两个事务按不同主键顺序加锁,比如事务 A 先锁 id=100 再锁 id=200,事务 B 反过来,就会触发 MySQL 自动检测并报错:Deadlock found when trying to get lock; try restarting transaction。
- 统一加锁顺序:所有业务路径严格按主键升序查询,例如先
SELECT ... WHERE id = LEAST(?, ?) FOR UPDATE,再查大 ID - 单次只锁一行:别在一个事务里对多个商品 ID 执行多次
FOR UPDATE - 应用层必须捕获该错误并重试,不能忽略或直接抛给前端
事务太长,锁就成瓶颈
锁持有时间越长,排队请求越多,TPS 断崖下跌。尤其不能在事务里调外部 HTTP 接口或做复杂计算。
- 设置合理超时:
innodb_lock_wait_timeout = 3(单位秒),避免一个慢事务卡死整条链路 - 事务内只做三件事:
SELECT ... FOR UPDATE→ 库存判断 →UPDATE→COMMIT,其他逻辑(如生成订单、发消息)移出事务 - 库存扣减失败时必须
ROLLBACK,不能靠连接关闭自动清理——很多连接池会复用连接
真正难的不是写对那条 FOR UPDATE,而是把事务边界、索引设计、错误重试、锁等待控制全部串起来。漏掉任意一环,系统就在高并发下悄悄超卖。


















