单条update带stock>=1条件是防超卖最稳最轻量方案:它在数据库层原子完成“判断+扣减”,自动加行锁,无需显式事务或隔离级别,执行后检查row_count()即可精准识别成功或库存不足。

UPDATE 带 stock >= 1 条件是防超卖最稳、最轻量的方案,不需要锁、不依赖事务隔离级别,也比 SELECT FOR UPDATE 更少踩坑。
为什么单条 UPDATE 就能防超卖
超卖不是因为没开事务,而是应用层把“查库存→判断→扣减”拆成三步,中间存在并发窗口。MySQL 的 UPDATE 语句天然原子:它先按 WHERE 定位行、加行锁、再执行赋值,整个过程不可打断。
-
UPDATE products SET stock = stock - 1 WHERE id = 123 AND stock >= 1这一句就完成了检查和扣减 - 执行后立刻读取
ROW_COUNT():1表示成功,0表示库存不足或商品不存在 - 别写
stock > 0—— 当stock = 0时,stock > 0为 false,但语义上“至少剩 1 件可卖”更准确对应stock >= 1 - 该语句自动触发 InnoDB 行锁,无需额外
BEGIN/COMMIT,autocommit=1 下也生效
SELECT FOR UPDATE 容易踩的坑
很多人以为加了锁就安全,但实际极易失效或拖垮系统:
- 必须走主键或唯一索引,否则可能升级为间隙锁甚至表锁(比如
WHERE category_id = 5且该字段无索引) - 锁持有时间 = 整个事务持续时间;中间夹个日志记录、RPC 调用或
sleep,锁就一直占着 - 两个事务都只
SELECT FOR UPDATE后不UPDATE,锁会在COMMIT才释放——此时第三个请求可能已绕过锁直接UPDATE成功 - 死锁真实存在:A 锁
id=1后想锁id=2,B 反过来,双方卡住
乐观锁在高并发下慎用 version 字段
乐观锁适合冲突率低的场景,但在电商结账这种高争抢路径里,重试成本高、失败率陡增:
- 每次失败都要重新查
stock和version,再发一次UPDATE,网络+数据库往返叠加 - 若重试逻辑没控制次数或退避,可能引发雪崩式重试压力
-
UPDATE ... WHERE version = ? AND stock > 0看似双重校验,但stock > 0仍需靠数据库层判断,不如直接用stock >= 1简洁 - 真正需要乐观锁的,是“读多写少+允许短暂不一致”的场景,比如商品详情页浏览计数
预留库存机制要拆开“预留”和“扣减”
Shopify 黑五峰值 $5.1M/min 的实践表明:超卖保护本质是两件事,不能混在一个 SQL 里解决:
- “预留”是支付开始时的短暂占用(通常几分钟),建议用独立表 +
SKIP LOCKED(MySQL 8.0+)实现细粒度行级抢占 - “扣减”是支付成功后的最终落库,必须走带
stock >= 1的原子UPDATE,并校验ROW_COUNT() - 别把“预留”和“扣减”塞进同一个事务——预留失败要快速返回,扣减失败要触发订单取消,逻辑必须解耦
- Redis 预减只适合秒杀等极端场景,最终一致性风险真实存在:Redis 挂了、异步落库失败、超时未回滚,都会导致库存不一致
真正难的不是写对那条 UPDATE,而是确保所有业务路径(下单、改价、退款、后台强制调拨)都走同一套原子校验逻辑,且任何分支都不漏掉 ROW_COUNT() 判断。











