正确做法是UPDATE时在WHERE中加入库存校验条件,如WHERE id = 123 AND stock >= 1,并检查ROW_COUNT();复杂逻辑需配合SELECT FOR UPDATE加行锁,且WHERE条件避免函数导致索引失效。

UPDATE WHERE 语句必须带库存校验
并发扣减库存最直接的错误,就是只写 UPDATE product SET stock = stock - 1 WHERE id = 123。这会导致超卖:两个请求同时读到 stock=1,各自执行减 1,最终变成 -1。
正确做法是把库存是否充足判断直接塞进 WHERE 条件里,让 MySQL 在更新前原子性校验:
UPDATE product SET stock = stock - 1 WHERE id = 123 AND stock >= 1;
执行后检查 ROW_COUNT()(MySQL 返回影响行数):
- 返回 1 → 扣减成功
- 返回 0 → 库存不足或记录不存在,需业务层拒绝下单
务必使用 SELECT FOR UPDATE 配合事务
当扣减逻辑不止一行 SQL(比如要先查价格、再扣库存、再写订单),单纯靠 WHERE 校验不够,必须加行锁防止并发读写冲突。
关键点:
- 必须在
START TRANSACTION内执行 -
SELECT ... FOR UPDATE会锁定该行(即使没命中索引,可能升级为表锁) - 锁持续到事务结束(
COMMIT或ROLLBACK),不是语句结束
示例:
START TRANSACTION; SELECT stock FROM product WHERE id = 123 FOR UPDATE; -- 此时其他事务对 id=123 的 SELECT FOR UPDATE / UPDATE 会被阻塞 UPDATE product SET stock = stock - 1 WHERE id = 123 AND stock >= 1; -- 检查 ROW_COUNT(),失败则 ROLLBACK COMMIT;
避免 WHERE 条件中使用函数或表达式导致索引失效
如果写成 WHERE id = ? AND stock - 1 >= 0,MySQL 无法用上 stock 索引(哪怕有联合索引),可能触发全表扫描+锁表,极大降低并发能力。
应始终保持 WHERE 中的列是独立出现的:
- ✅
WHERE id = 123 AND stock >= 1(能走PRIMARY KEY+ 范围条件) - ❌
WHERE id = 123 AND stock - 1 >= 0(stock - 1是表达式,索引失效) - ❌
WHERE id = 123 AND stock > 0(虽然语义等价,但某些旧版本优化器可能不走索引,>= 1更稳)
高并发场景下慎用自增 ID 做库存分片依据
有人想用 id % 4 把商品分散到不同行来缓解热点,但这会破坏业务语义(同一商品多行库存难聚合),且无法解决单商品高并发问题。
真正有效的分片思路是:
- 按业务维度拆分:比如「SKU + 仓库编码」作为联合主键,不同仓库存独立扣减
- 用 Redis 预减库存(异步落库),但需处理 Redis 故障/回滚一致性问题
- 数据库端改用乐观锁(version 字段)+ 重试,适合冲突率低的场景
纯 MySQL 方案里,UPDATE ... WHERE ... AND stock >= N + 事务 + 索引覆盖,已经能扛住几千 QPS 的秒杀流量;再往上,就得考虑缓存、队列、分库分表了。










