补货Skill实战指南:从零构建高可靠库存管理系统

4次阅读
没有评论

共计 2460 个字符,预计需要花费 7 分钟才能阅读完成。

image.webp

背景痛点:电商库存的生死时速

去年双 11 我们系统经历了惨痛的教训:某爆款商品超卖 2000 件,直接导致 50 万元赔偿损失。事后复盘发现三个典型问题:

补货 Skill 实战指南:从零构建高可靠库存管理系统

  1. 超卖现象 :1000 件库存最终卖出 1200 单,因为减库存的 SQL 没有加锁
  2. 数据不一致 :订单系统显示已付款,库存系统却显示有货
  3. 补货延迟 :凌晨 3 点库存触底,直到早上 9 点才人工补货

这促使我们开发了现在的补货 Skill 系统,其核心要解决两个问题:

  • 如何保证库存操作的原子性(不超卖)
  • 如何实现智能化的自动补货

技术方案选型:锁的艺术

Redis 分布式锁 vs 数据库乐观锁

我们对比了两种主流方案:

  1. Redis 分布式锁 (最终采用方案)
  2. 优点:性能高(单节点 10w+ QPS),实现简单
  3. 缺点:需要处理锁续期问题
  4. 关键代码片段(Go 版):

    // 获取锁(含自动续期)func acquireLock(redisClient *redis.Client, key string, ttl time.Duration) (bool, error) {result, err := redisClient.SetNX(key, "locked", ttl).Result()
        if err != nil || !result {return false, err}
    
        // 启动续期 goroutine
        go func() {ticker := time.NewTicker(ttl / 2)
            defer ticker.Stop()
            for {
                select {
                case <-ticker.C:
                    redisClient.Expire(key, ttl).Result()
                case <-stopChan:
                    return
                }
            }
        }()
        return true, nil
    }

  5. 数据库乐观锁

  6. 优点:无需额外组件,适合低并发场景
  7. 缺点:高并发时大量失败重试
  8. SQL 示例:
    UPDATE inventory SET stock = stock - 1, version = version + 1 
    WHERE sku_id = '1001' AND version = 123 AND stock >= 1;

补货触发条件设计

我们的补货策略采用多维度触发机制:

  • 静态阈值 :当库存低于安全库存(如总库存的 20%)
  • 动态预测 :基于过去 7 天销量预测未来 3 天需求
  • 时间窗口 :凌晨 2 - 4 点不触发自动补货(避开对账时段)

核心实现:消息驱动架构

库存扣减流程

  1. 订单服务发起扣减请求
  2. 库存服务获取分布式锁
  3. 执行 CAS 操作检查库存
  4. 发布库存变更事件

架构示意图:

sequenceDiagram
    订单服务 ->>+ 库存服务: 扣减请求 (SKU=1001, 数量 =1)
    库存服务 ->>+Redis: 获取锁 (sku:1001)
    Redis-->>- 库存服务: 获取成功
    库存服务 ->> 数据库: CAS 更新库存
    数据库 -->> 库存服务: 更新成功
    库存服务 ->>MQ: 发布库存变更事件
    库存服务 ->>Redis: 释放锁 

异步补货流程代码(Java 版)

// 库存监听器
@RabbitListener(queues = "inventory.queue")
public void handleLowStockEvent(InventoryEvent event) {
    // 检查是否需要补货(防重复触发)if (!needReplenishment(event.getSkuId())) {return;}

    // 创建补货任务(含幂等 ID)ReplenishmentTask task = new ReplenishmentTask(UUID.randomUUID().toString(),
        event.getSkuId(),
        calculateReplenishAmount(event.getSkuId())
    );

    // 持久化任务状态
    taskRepository.save(task);

    // 触发供应商接口调用
    supplierService.requestReplenishment(task);
}

// 幂等检查(基于 Redis 实现)private boolean needReplenishment(String skuId) {
    String key = "replenish:check:" + skuId;
    return redisTemplate.opsForValue().setIfAbsent(key, "1", 24, TimeUnit.HOURS);
}

生产环境实战经验

性能压测数据

我们使用 JMeter 对不同方案进行了对比测试(单商品 10 万并发):

方案 平均响应时间 成功率 QPS
无锁 15ms 62% 8500
数据库悲观锁 210ms 99.9% 1200
Redis 分布式锁 45ms 99.8% 6800
乐观锁 + 重试 75ms 99.5% 3200

故障恢复方案

我们遇到过两次严重问题及解决方案:

  1. 补货消息丢失
  2. 现象:MQ 集群故障导致补货请求丢失
  3. 方案:增加定时任务扫描低库存商品
  4. 代码:

    /* 每小时执行的低库存扫描 */
    SELECT sku_id FROM inventory 
    WHERE stock < safe_stock 
    AND last_modified > NOW() - INTERVAL 1 DAY

  5. ABA 问题

  6. 现象:取消订单回补库存时版本号被其他操作覆盖
  7. 方案:使用复合版本号(时间戳 + 计数器)

避坑指南

时钟漂移问题

在分布式锁场景下,我们曾因 NTP 同步问题导致:

  • 服务器 A 认为锁已过期(实际未过期)
  • 服务器 B 仍持有锁时被 A 强行获取

解决方案:

  • 采用 Redis 的 RedLock 算法(多实例部署)
  • 增加时钟漂移检测机制

补货状态机设计

为防止补货循环触发,我们设计了明确的状态流转:

stateDiagram-v2
    [*] --> IDLE
    IDLE --> PENDING: 库存低于阈值
    PENDING --> PROCESSING: 获取补货权
    PROCESSING --> SUCCESS: 供应商确认
    PROCESSING --> FAILED: 供应商拒绝
    FAILED --> PENDING: 30 分钟后重试 

开放性问题

当前系统仍存在一个挑战:当商品同时在多个仓库有库存时,如何设计补货策略?考虑因素包括:

  • 各仓库实时库存水平
  • 区域销售预测差异
  • 物流成本计算
  • 供应商最短到货时间

我们正在试验基于强化学习的动态调配算法,期待读者分享你们的解决方案。

正文完
 0
评论(没有评论)