原子update比select+update更可靠,因其在innodb中自动加x锁,将库存校验与扣减合并在单条语句内原子执行,无中间窗口;而select for update需显式持锁、易引发串行化瓶颈与死锁。

直接用 UPDATE ... WHERE stock >= ? 原子语句,比先 SELECT FOR UPDATE 再 UPDATE 更可靠、更轻量,且天然防超卖。
为什么原子 UPDATE 比 SELECT + UPDATE 更适合高并发
很多人默认“先查再改”更直观,但库存扣减不是普通业务逻辑——它本质是“检查并修改同一行状态”的原子动作。在高并发下,SELECT ... FOR UPDATE 会显式持锁,锁时间越长,阻塞越严重;而 UPDATE ... WHERE stock >= ? 是单条语句:InnoDB 自动对命中行加 X 锁,WHERE 条件校验和更新在同一个原子操作内完成,没有中间窗口。
关键点:
-
WHERE条件必须包含索引字段(如sku_id),否则可能升级为间隙锁甚至表锁 - 返回的
ROW_COUNT()为 0 时,明确表示“库存不足或已被扣完”,不是错误,而是业务预期结果 - 无需手动
START TRANSACTION包裹(除非后续还要跨表操作),单条UPDATE本身就是事务性语句
如何写一个真正生产可用的扣减存储过程
重点不是“功能全”,而是“失败快、不留副作用、不拖慢”。下面这个骨架去掉所有冗余,只保留核心安全逻辑:
DELIMITER $$
CREATE PROCEDURE deduct_stock_safe(
IN p_sku_id BIGINT,
IN p_num INT,
OUT p_result_code TINYINT
)
BEGIN
DECLARE affected_rows INT DEFAULT 0;
<pre class="brush:php;toolbar:false;">-- 原子扣减:stock 为 UNSIGNED,WHERE 天然拦截负数更新
UPDATE inventory
SET stock = stock - p_num, updated_at = NOW()
WHERE sku_id = p_sku_id AND stock >= p_num;
-- 检查影响行数
SET affected_rows = ROW_COUNT();
IF affected_rows = 1 THEN
SET p_result_code = 0; -- 成功
ELSE
SET p_result_code = -1; -- 库存不足(注意:不是 SQL 错误)
END IF;END$$
使用时只需调用:CALL deduct_stock_safe(123, 5, @code),然后查 @code 即可。不要在里面写日志、发消息、调外部服务——那些都该由应用层异步处理。
容易踩的坑:锁、类型、返回值
这三个地方出错,会导致看似正常实则不可靠:
-
stock字段没设为UNSIGNED:当UPDATE执行时若因并发导致stock - p_num为负,MySQL 会静默截断为 0 或报错(取决于 SQL mode),而不是让 WHERE 条件失效。设为UNSIGNED后,stock >= p_num不成立时整条语句不生效,这才是你想要的行为 - WHERE 中的
sku_id没建唯一索引:InnoDB 的行锁依赖索引定位。无索引或非唯一索引时,UPDATE可能锁住整个范围,甚至锁表,QPS 直线下降 - 用
SELECT ... INTO获取扣减前库存再判断:这又退回了 check-then-act 模式。哪怕加了FOR UPDATE,只要判断和更新不在同一语句,就存在被其他事务插队的窗口
什么时候才需要 SELECT FOR UPDATE
仅当你要做**多步强一致决策**时才用,比如:“扣减库存 + 更新销量 + 写入赠品规则 + 校验用户等级是否满足免邮”,且这些步骤必须全部成功或全部失败。此时需显式 BEGIN,第一句就是 SELECT ... FOR UPDATE,后面所有操作都在同一事务中完成,并配好 SIGNAL 或 ROLLBACK。
但纯库存扣减?不需要。它本身就是一个“读-判-改”三合一的原子操作,UPDATE ... WHERE 已经把它干完了。强行加锁,只是把瓶颈从数据库一致性,转移到了锁竞争上。










