库存扣减必须用事务包裹,否则必然超卖;正确做法是在begin/commit事务中首句执行select ... for update(where字段需有唯一索引),再校验并原子更新,失败时显式rollback或signal。

库存扣减必须用事务包裹,否则数据必然不一致
直接执行 UPDATE 语句扣减库存,哪怕加了 WHERE stock >= ? 条件,也无法防止并发超卖。MySQL 的 SELECT ... FOR UPDATE 在非唯一索引或未命中索引时可能锁表,而存储过程里若只靠应用层判断再更新,中间窗口期会被多个请求钻空。真正安全的做法是:在存储过程中开启事务,用行级锁锁定目标商品记录,并在同个事务内完成查询、校验、扣减、日志写入全流程。
MySQL 存储过程里用 SELECT ... FOR UPDATE 锁住库存行
关键不是“有没有锁”,而是“锁得准不准”。常见错误是先 SELECT stock FROM inventory WHERE sku = ?,再 UPDATE —— 这两步之间没有锁,别人可以插进来。正确做法是在事务中第一句就用 SELECT stock INTO @cur_stock FROM inventory WHERE sku = ? FOR UPDATE。注意三点:
-
FOR UPDATE必须配合事务(BEGIN/COMMIT),单独执行无效 -
WHERE条件字段(如sku)必须有唯一索引,否则会升级为间隙锁甚至表锁 - 不要在
SELECT ... FOR UPDATE后再做复杂计算或调用外部服务,锁持有时间越长,阻塞越严重
扣减失败时必须显式 ROLLBACK,不能依赖自动回滚
MySQL 默认不会因业务逻辑判断(比如库存不足)自动回滚事务。很多人写成:
IF @cur_stock <p>这只会返回提示,事务仍处于打开状态,后续语句继续执行,最终可能 <code>COMMIT</code> 一个没扣减却写了日志的“假成功”。正确写法是:</p>
- 用
IF @cur_stock - 或手动
ROLLBACK; LEAVE proc_label;(需提前定义proc_label:) - 避免在存储过程里用
INSERT INTO log_table记录“失败日志”后再COMMIT—— 失败了就不该留任何副作用
高并发下 GET_LOCK() 不是替代方案,反而更危险
有人想绕过行锁,用 GET_LOCK('stock_sku_123', 0) 做应用级互斥。这问题很大:
-
GET_LOCK是会话级锁,连接断开自动释放,连接池复用时极易误释放 - 锁名字符串拼接容易出错(比如未转义特殊字符),导致不同 SKU 锁冲突或漏锁
- 它不和事务集成,无法保证锁期间的数据一致性,仍需自己处理重复扣减
- 性能比行锁差,且无法利用 InnoDB 的死锁检测机制,容易卡死
真正难的是锁粒度与业务节奏的匹配——比如秒杀场景要锁单 SKU,但批量订单可能需锁多个 SKU,这时得用临时表+循环加锁,或拆成多次原子操作。这些细节没写进存储过程,光靠 SQL 本身解决不了。











