Appearance
库存管理
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⚠️ 注意事项
- 原子性: 所有库存操作必须通过 Lua 脚本保证原子性
- 锁超时: 分布式锁设置合理的 TTL,防止死锁
- 库存同步: 活动结束后需同步 Redis 库存到数据库
- 预警阈值: 通过系统配置设置库存预警阈值