库存扣减问题
最近突然又看到这个问题,记录一下
解决一个问题要牢记我们的核心目的,库存扣减问题的核心目的很简单,就是保证库存不超买,那如何保证那,下面来一一讨论
只用数据库
- 先使用select来查询这个库存数量
- 然后使用update来扣减库存 大概样子是 update set a=a-1 table tableName
首先我们要确保这两个操作是一块执行的,那就是要放入到一个事务内。当高并发的时候,这样做的会让数据库性能严重下降,因为目前基本上是顺序执行的,可以通过主从结构来稳住数据库性能,比如可以使用主库来写数据,从库来读数据
数据库+java
程序中计算扣减后的值,直接用update stock set stock_num=$result where sid=$sid更新
问题:并发时多线程同时查库存,后扣减操作会覆盖先扣减结果
CAS 乐观锁
先查库存,更新时校验当前库存是否与查询值一致(update ... where sid=$sid and stock_num=$num_old),可加版本号避免 ABA 问题
问题:无并发问题,但高并发下大量请求失败需重试,可能超时、吞吐量下降
redis+数据库
用 Redis 的 DECR 命令或 Lua 脚本实现原子扣减,异步记录日志并更新数据库。
核心流程:先写 log 库记录扣减信息,再用 Redis 扣减缓存,最后通过监听 log 库的 binlog 异步更新库存数据库
问题:需处理 Redis 宕机数据恢复(重启后用数据库重建缓存)
这个开启redis的rdb和aof即可
完整的流程如下:
- 幂等校验 上游系统发起扣减请求时,携带唯一订单 ID(如订单号),先查询 log 库是否已有该订单的扣减记录。若已存在,直接返回成功(防重复扣减);若不存在,进入下一步。
- 开启事务写 log 库 在数据库事务中,向 log 库插入一条扣减记录,包含订单 ID、商品 ID、扣减数量等信息(如{"扣减号":uuid, "skuid1":数量, "skuid2":数量})。此时事务未提交,记录处于 “未确认” 状态
- Redis 执行原子扣减 通过 Redis 的 Lua 脚本批量扣减缓存中的库存(保证多个商品扣减的原子性)。若扣减成功(库存充足),则继续;若扣减失败(库存不足),则直接回滚步骤 2 的事务,log 库不保留记录,返回扣减失败
- 提交 log 库事务 确认 Redis 扣减成功后,提交步骤 2 的事务,log 库中的扣减记录正式落库(此时记录可被其他服务读取)
- 异步同步至库存数据库 监听 log 库的 binlog 日志,当检测到新的扣减记录(步骤 4 提交的事务),由同步服务解析 binlog,将扣减信息转换为库存数据库的更新操作(如update stock set stock_num=stock_num-xxx where sid=xxx),同时生成流水记录存档。
mq+redis+数据库
用支持事务消息的 MQ(如 RocketMQ)替代 log 库,扣减成功后发送 commit 消息,失败则发送 rollback
优点:消息并发更高,编码更简单,通过 MQ 事务保证消息不丢失(消费者处理完后发送 ack)
完整流程如下:
- 幂等校验:同 log 库法,用订单 ID 查询 log 库,判断是否重复扣减
- 本地事务:写 log 库 + 发送 MQ 消息(可靠消息)
- 开启数据库事务,向 log 库插入扣减记录(状态为 “待处理”)
- 通过 “本地消息表 + MQ” 的可靠消息机制(如 Seata 的 TCC 或 RocketMQ 的事务消息),向 MQ 发送一条 “扣减指令” 消息(包含商品 ID、扣减数量等)
- 若 MQ 消息发送成功(确认消息已被 MQ 持久化),则提交 log 库事务(记录状态改为 “已发送”);若发送失败,回滚 log 库事务,避免 “有记录但无消息” 的不一致
- Redis 原子扣减:同 log 库法,扣减成功则继续,失败则回滚
- MQ 消费者处理库存数据库更
- 消费者监听 MQ 的 “扣减指令” 消息,接收后执行库存数据库的扣减操作(update stock set num=num-xxx where sid=xxx)
- 扣减成功后,向 log 库发送确认(更新记录状态为 “已完成”);若失败,根据重试策略重新消费(或人工介入)