如何在MySQL+Laravel中处理库存读写的竞态条件问题?
解决销售订单库存扣减的竞态问题
针对你遇到的多job并发处理订单导致的库存竞态问题,以下是几种实用的解决方案:
1. 数据库行级悲观锁
直接在数据库层面锁定目标产品的库存记录,确保同一时间只有一个job能修改该记录:
- 核心逻辑:将库存读取与扣减操作放在同一个事务中,用
SELECT ... FOR UPDATE锁定行 - 示例SQL:
-- 开启事务 BEGIN; -- 锁定PRODUCTA的库存行,其他请求会阻塞直到锁释放 SELECT stock FROM product_inventory WHERE product_id = 'PRODUCTA' FOR UPDATE; -- 执行库存扣减(你已处理库存不足判断,直接执行更新即可) UPDATE product_inventory SET stock = stock - {order_quantity} WHERE product_id = 'PRODUCTA'; -- 提交事务 COMMIT;
- 注意:事务要尽可能短,避免长时间占用锁影响并发性能。
2. 数据库乐观锁
无需显式加锁,通过版本号或库存值比对实现并发控制,适合高并发场景:
- 核心逻辑:读取库存时获取版本号,更新时仅当版本号未变化才生效,失败则重试
- 示例SQL:
-- 读取库存和当前版本号 SELECT stock, version FROM product_inventory WHERE product_id = 'PRODUCTA'; -- 尝试更新库存,仅当版本号与读取时一致才执行 UPDATE product_inventory SET stock = stock - {order_quantity}, version = version + 1 WHERE product_id = 'PRODUCTA' AND version = {read_version};
- 如果更新返回的影响行数为0,说明其他job已修改库存,需要重新读取数据并重试扣减操作,重试次数建议设为3-5次。
3. 分布式锁(多节点部署场景)
如果你的job运行在多个服务器节点上,数据库行锁无法跨节点生效,可使用分布式锁:
- 核心逻辑:处理PRODUCTA订单前,先获取针对该产品的分布式锁,操作完成后释放锁
- 示例(基于Redis):
# 伪代码 lock_key = "lock:product:PRODUCTA" # 获取锁,设置过期时间(需大于库存处理的最长耗时) if redis_client.set(lock_key, "locked", nx=True, ex=30): try: # 执行库存读取、扣减、更新操作 pass finally: # 释放锁 redis_client.delete(lock_key) else: # 未获取到锁,等待重试或直接返回 pass
- 注意:要处理锁超时的情况,避免因job异常导致锁无法释放。
4. 订单串行化处理
将同一产品的订单请求放入单独的消息队列,让jobs依次处理:
- 核心逻辑:所有针对PRODUCTA的订单处理请求都发送到专属队列,单个消费者依次处理队列中的任务
- 优势:逻辑简单,从根源上避免竞态;劣势:若某产品订单量过大,可能出现队列堆积,需根据业务量级评估是否适用。
通用注意事项
- 所有库存扣减操作必须保证原子性,要么全部成功,要么回滚到初始状态
- 乐观锁的重试机制要设置合理次数,避免无限重试消耗资源
- 无论采用哪种方案,都要做好异常日志记录,方便排查并发问题
内容的提问来源于stack exchange,提问作者Muhammad Adnan
相关产品推荐
相关产品推荐

