mysql事务并发下如何扣减库存_mysql业务实现示例

15次阅读

select … for update 可防止超卖,需满足 innodb 引擎和索引查询,配合 update 带库存校验条件、read committed 隔离级及死锁重试机制。

mysql 事务并发下如何扣减库存_mysql 业务实现示例

SELECT …… FOR UPDATE 防止超卖

在并发扣减库存场景下,不加锁直接 UPDATE 极易导致超卖。核心做法是先查再锁再改:用 SELECT …… FOR UPDATE 在事务内锁定目标行,确保同一商品不会被多个事务同时修改。

注意必须满足两个前提:表引擎为 InnoDB;查询条件命中索引(如主键或唯一索引),否则会升级为表锁,严重拖慢性能。

  • 不要写 SELECT * FROM stock WHERE sku = 'A001' 后再判断,这中间存在竞态窗口
  • 务必把 SELECT …… FOR UPDATE 和后续 UPDATE 放在同一个事务中(BEGIN → 查询 → 更新 → COMMIT
  • 如果查询不到记录(sku 不存在),FOR UPDATE 不会报错,但后续 UPDATE 将影响 0 行,需主动检查 ROW_COUNT()

扣减逻辑必须带库存校验条件

仅靠行锁不能替代业务校验。即使锁住了某行,也得确认当前库存是否足够——否则可能扣成负数。正确方式是在 UPDATE 语句里直接写条件判断。

UPDATE stock  SET quantity = quantity - 1  WHERE sku = 'A001' AND quantity >= 1;

执行后检查 ROW_COUNT() 返回值:

  • 返回 1:扣减成功
  • 返回 0:库存不足或记录不存在,事务应主动回滚

避免先 SELECT quantityUPDATE,那又回到竞态问题。

事务隔离级别建议用 READ COMMITTED

MySQL 默认是 REPEATABLE READ,它会使用间隙锁(Gap Lock)防止幻读,但在库存类场景中容易引发不必要的锁等待甚至死锁。而 READ COMMITTED 只锁实际命中的行,无间隙锁,更轻量。

设置方式(连接级):

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
  • 不需要全局改,应用在扣减库存的数据库连接池初始化时设置即可
  • 确认业务能接受“同一事务中两次读可能不一致”,这对下单这种短事务通常无影响
  • 如果已用 SELECT …… FOR UPDATEREAD COMMITTED 下锁行为更可预测,不易误锁相邻范围

别忽略连接超时和死锁重试机制

高并发下,FOR UPDATE 等待锁、网络延迟、事务过长都可能导致超时或死锁。MySQL 报错典型有:

  • Lock wait timeout exceededinnodb_lock_wait_timeout 触发)
  • Deadlock found when trying to get lock

应用层必须捕获这些错误并实现简单重试(比如最多 3 次),而不是直接抛给用户。伪代码逻辑:

for retry in range(3):     try:         BEGIN         SELECT quantity FROM stock WHERE sku = ? FOR UPDATE;         UPDATE stock SET quantity = quantity - 1 WHERE sku = ? AND quantity >= 1;         COMMIT         break     except (DeadlockError, LockTimeoutError):         time.sleep(0.05 * (2 ** retry))  # 指数退避         continue

重试前强制断开并重建连接,避免复用处于异常状态的事务上下文。

text=ZqhQzanResources