Skip to content

库存管理

MineShop 的库存系统采用 Redis + Lua 原子操作,支持普通商品、秒杀、团购三种库存类型,确保高并发下的库存安全。

🎯 系统架构

┌─────────────────────────────────────────────────────────────┐
│                      库存服务层                               │
│                 DomainOrderStockService                      │
├─────────────────────────────────────────────────────────────┤
│  普通库存           │  秒杀库存           │  团购库存          │
│  product:stock     │  seckill:stock:*   │  groupbuy:stock:* │
├─────────────────────────────────────────────────────────────┤
│                      Redis Hash 存储                         │
│                   field=skuId, value=库存数量                 │
└─────────────────────────────────────────────────────────────┘

✨ 核心特性

1. 三种库存类型

类型Redis Key说明
普通商品product:stock常规商品库存
秒杀库存seckill:stock:{sessionId}秒杀场次库存
团购库存groupbuy:stock:{groupBuyId}团购活动库存

2. Lua 原子扣减

lua
-- 原子扣减库存脚本
local stockKey = KEYS[1]

-- 第一轮:检查所有 SKU 库存
for i = 1, #ARGV, 2 do
    local field = ARGV[i]
    local quantity = tonumber(ARGV[i + 1])
    local current = tonumber(redis.call('HGET', stockKey, field) or '-1')
    if current < quantity then
        return 0  -- 库存不足
    end
end

-- 第二轮:批量扣减
for i = 1, #ARGV, 2 do
    local field = ARGV[i]
    local quantity = tonumber(ARGV[i + 1])
    redis.call('HINCRBY', stockKey, field, -quantity)
end

return 1  -- 扣减成功

3. 分布式锁

php
// DomainOrderStockService.php
public function acquireLocks(array $items, string $stockHashKey): array
{
    $locks = [];
    foreach (array_keys($this->normalizeItems($items)) as $skuId) {
        $lockKey = sprintf('mall:stock:lock:%s:%d', $stockHashKey, $skuId);
        $token = Str::uuid()->toString();
        
        for ($i = 0; $i < $this->lockRetry; ++$i) {
            $acquired = (bool) $this->redis()->set(
                $lockKey, $token, ['NX', 'PX' => $this->lockTtl]
            );
            if ($acquired) {
                break;
            }
            usleep(50_000);  // 50ms 重试间隔
        }
        
        if (!$acquired) {
            $this->releaseLocks($locks);
            throw new \RuntimeException('库存繁忙,请稍后重试');
        }
        $locks[$lockKey] = $token;
    }
    return $locks;
}

🔄 库存操作流程

┌──────────┐    获取锁    ┌──────────┐    Lua扣减    ┌──────────┐
│  下单请求 │ ──────────→ │  持有锁   │ ───────────→ │  扣减成功 │
└──────────┘             └──────────┘              └──────────┘
      │                        │                        │
      │ 锁获取失败              │ 库存不足               │ 释放锁
      ↓                        ↓                        ↓
┌──────────┐            ┌──────────┐              ┌──────────┐
│  返回繁忙 │            │  返回失败 │              │  创建订单 │
└──────────┘            └──────────┘              └──────────┘

📦 核心服务

php
// DomainOrderStockService.php
final class DomainOrderStockService
{
    /**
     * 预扣库存(原子操作)
     */
    public function reserve(array $items, string $stockHashKey): void
    {
        $normalized = $this->normalizeItems($items);
        if ($normalized === []) {
            throw new \RuntimeException('没有可扣减的商品');
        }

        $args = [];
        foreach ($normalized as $skuId => $quantity) {
            $args[] = (string) $skuId;
            $args[] = (string) $quantity;
        }

        $result = $this->redis()->eval(self::DEDUCT_SCRIPT, $payload, 1);
        if ((int) $result !== 1) {
            throw new \RuntimeException('库存不足或商品已下架');
        }

        // 触发库存预警
        $this->triggerStockWarnings($normalized, $stockHashKey);
    }

    /**
     * 回滚库存
     */
    public function rollback(array $items, string $stockHashKey): void
    {
        $normalized = $this->normalizeItems($items);
        foreach ($normalized as $skuId => $quantity) {
            $this->redis()->hIncrBy($stockHashKey, (string) $skuId, $quantity);
        }
    }

    /**
     * 解析库存 Key
     */
    public static function resolveStockKey(string $orderType, int $activityId = 0): string
    {
        return match ($orderType) {
            'seckill' => sprintf('%s:%d', self::SECKILL_STOCK_PREFIX, $activityId),
            'group_buy' => sprintf('%s:%d', self::GROUP_BUY_STOCK_PREFIX, $activityId),
            default => self::STOCK_HASH_KEY,
        };
    }
}

🛡️ 库存预警

php
private function triggerStockWarnings(array $deducted, string $stockHashKey): void
{
    // 仅普通商品库存触发预警
    if ($stockHashKey !== self::STOCK_HASH_KEY) {
        return;
    }

    $threshold = $this->mallSettingService->product()->stockWarning();
    if ($threshold <= 0) {
        return;
    }

    foreach (array_keys($deducted) as $skuId) {
        $remaining = (int) $this->redis()->hGet($stockHashKey, (string) $skuId);
        if ($remaining <= $threshold) {
            event(new ProductStockWarningEvent($skuId, $remaining, $threshold));
        }
    }
}

📊 Redis 数据结构

# 普通商品库存
HGETALL product:stock
> "101" => "500"    # SKU 101 库存 500
> "102" => "200"    # SKU 102 库存 200

# 秒杀库存
HGETALL seckill:stock:1
> "201" => "100"    # 场次1 SKU 201 库存 100

# 团购库存
HGETALL groupbuy:stock:5
> "301" => "50"     # 团购5 SKU 301 库存 50

⚠️ 注意事项

  1. 原子性: 所有库存操作必须通过 Lua 脚本保证原子性
  2. 锁超时: 分布式锁设置合理的 TTL,防止死锁
  3. 库存同步: 活动结束后需同步 Redis 库存到数据库
  4. 预警阈值: 通过系统配置设置库存预警阈值

📚 相关文档

基于 Apache-2.0 许可发布 | 感谢 MineAdmin 提供的优秀基础框架