库存扣减问题

0 阅读1分钟

主要解决问题

锁粒度 性能瓶颈问题
    锁竞争、数据库行锁开销、雪崩
事务ACID 数据一致性问题
    超卖、少卖、库存不一致
缓存和数据库双写不一致问题
    数据不一致、库存预热与回退问题
高并发 其他相关业务问题
    重复扣款、下单、恶意请求

问题一:性能瓶颈问题

减少锁的竞争

  • 库存分片,通过hash或轮询随机算法等将库存数据分片存储到不同的数据库实例中

    如库存总量 1000,分为 10 个段,每段 100)

  • 锁粒度(乐观锁代替悲观锁)

    数据库层面使用版本号(version)或 CAS(Compare And Set)方式,避免长事务持有行锁

    UPDATE stock SET count = count - #{num}, version = version + 1 WHERE id = #{id} AND version = #{oldVersion} AND count >= #{num}

降低数据库行锁开销

  • 缓存预热 + 异步同步

将库存预热到 Redis,扣减请求先在 Redis 中完成,然后通过消息队列(如 RocketMQ、Kafka)异步将最终结果落库

请求到达 → Redis 扣减(Lua 脚本保证原子性) → 返回成功
发送“库存扣减成功”消息到 MQ
消费端批量消费消息,合并更新数据库(如每 100 条一次批量 update
  • 数据库行锁优化

    确保扣减 SQL 走索引(如主键或唯一索引),避免表锁或间隙锁 使用 SELECT ... FOR UPDATE 时,务必在事务中尽快提交,减少锁持有时间

雪崩

限流->将请求拦截在系统前端,避免直接冲击数据库
  (使用令牌桶、漏桶算法,限制单位时间内的请求量,保护后端系统)
熔断降级->当依赖的 Redis 或数据库出现超时、异常时,启动熔断器
多级缓存->

问题二:数据一致性问题

异步落库(导致数据库库存未扣减)
重复扣减
部分失败(扣减库存后,后续业务(如创建订单)失败)
缓存与数据库同步延迟(期间查询可能出现不一致)

原子扣减(双重保障)

Redis Lua 脚本
数据库乐观锁:落库时再次校验库存,防止异步过程中出现超卖

异步落库的一致性保障

本地消息表:在业务数据库中建一张消息表,将扣减记录和消息状态放在同一本地事务中
事务消息

幂等处理

全局唯一流水号:每次扣减操作生成唯一 ID(如订单号+商品ID),在消费端根据流水号去重
Redis 预检查:消费前先查 Redis 是否已处理过该流水号

库存回滚

消息驱动回滚:发送“库存回滚”消息,消费端执行库存增加
定时对账补偿:扫描超时未支付的订单,调用回滚接口
TCC 分布式事务:若业务要求强一致性

通常秒杀场景可接受短暂不一致(最终一致),优先保证高并发

问题三:缓存和数据库双写不一致问题

延时双删(针对先删缓存、再更新数据库的优化)

允许短时间不一致,但希望最终一致

先删除缓存。
更新数据库。
延时一段时间(如 500ms),再次删除缓存


redis.delete(key);
database.update(data);
// 延迟执行,可采用线程池或消息队列
executor.schedule(() -> redis.delete(key), 500, TimeUnit.MILLISECONDS);

基于消息队列的异步更新(最终一致性)

对一致性要求较高,可接受短暂不一致,但需保证最终一致

更新数据库后,发送一条“更新缓存”的消息到消息队列,由消费者异步更新缓存。
    若更新失败,可重试
解耦,可靠性高(消息持久化),重试机制保证最终一致性


@Transactional
public void updateData(Data data) {
    database.update(data);
    // 事务提交后发送消息
    transactionSynchronization.registerSynchronization(new TransactionSynchronization() {
        public void afterCommit() {
            mqSender.send("cache_update", data);
        }
    });
}
// 消费者
@RabbitListener
public void handleCacheUpdate(Data data) {
    redis.set(data.getId(), data);
}

强一致性方案(分布式锁 / 读写锁)

对一致性要求极高(如金融交易),但并发量较低

对同一数据的读写操作加分布式锁,保证同一时刻只有一个线程能读写数据

问题四:其他相关业务问题

幂等性设计

全局唯一流水号:每次扣减请求携带唯一标识(如订单号、业务流水号),在服务端记录已处理的流水号

Redis 去重:使用 Redis SETNX 命令,将流水号作为 key 存储,
    设置合理过期时间,重复请求直接拒绝。
数据库去重表:建一张去重表,以流水号作为唯一索引,
    扣减前先插入,若主键冲突则说明已处理。

业务状态机:订单状态流转(如待支付、已支付、已取消),只有在特定状态才允许扣减库存,避免重复操作

库存扣减补偿机制

  • 定时任务扫描

    定期扫描超时未支付的订单,调用库存服务回滚接口(增加库存)。注意要处理并发问题,避免重复释放

  • 延迟消息

    下单成功后发送一条延迟消息

  • 状态机驱动

    订单状态变更时(如取消、超时关闭),主动触发库存回滚