Appearance
秒杀系统
MineShop 的秒杀系统采用活动 → 场次 → 商品三级管理模式,结合 Redis + Lua 原子操作,实现高并发下的库存安全扣减。
🎯 系统架构
┌─────────────────────────────────────────────────────────────┐
│ 秒杀活动 (Activity) │
│ • 活动名称、时间范围 │
│ • 活动状态、启用开关 │
├─────────────────────────────────────────────────────────────┤
│ 场次1 (Session) │ 场次2 (Session) │ ... │
│ 10:00-12:00 │ 14:00-16:00 │ │
├─────────────────────────────────────────────────────────────┤
│ 商品A │ 商品B │ 商品C │ 商品D │ 商品E │ ... │
│ ¥99 │ ¥199 │ ¥299 │ ¥399 │ ¥499 │ │
└─────────────────────────────────────────────────────────────┘✨ 核心特性
1. 三级管理结构
| 层级 | 说明 | 主要字段 |
|---|---|---|
| 活动 | 秒杀活动的顶层容器 | 名称、描述、开始/结束时间、状态 |
| 场次 | 活动下的时间段 | 开始/结束时间、限购数量、状态 |
| 商品 | 场次下的秒杀商品 | 原价、秒杀价、库存、已售数量 |
2. Lua 原子扣库存
lua
-- deduct_stock.lua
-- 原子扣减库存,防止超卖
-- 第一轮:检查所有 SKU 库存是否充足
for i = 1, #ARGV, 2 do
local field = ARGV[i]
local quantity = tonumber(ARGV[i + 1])
local current = tonumber(redis.call('HGET', KEYS[1], field) or 0)
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', KEYS[1], field, -quantity)
end
return 1 -- 扣减成功3. 缓存预热机制
php
// SeckillCacheService.php
public function warmSession(int $sessionId): void
{
// 1. 缓存场次信息
$this->cache->set(
$this->sessionKey($sessionId),
json_encode($model->toArray()),
['EX' => self::TTL]
);
// 2. 缓存商品列表 (Hash)
$this->cache->hMset($productsKey, $fields);
// 3. 预热库存 (Hash)
$this->warmStockHash($sessionId, $products);
}4. 30分钟编辑锁定
活动开始前 30 分钟进入缓存预热期,此时禁止编辑和删除:
php
// SeckillSessionEntity.php
public function isWithinCacheWarmupPeriod(): bool
{
$startTime = Carbon::parse($this->startTime);
$now = Carbon::now();
// 开始时间已过,不在预热期
if ($startTime->lte($now)) {
return false;
}
// 开始前 30 分钟内
return $startTime->diffInMinutes($now) <= 30;
}
public function canBeEdited(): bool
{
// 已激活或已结束不可编辑
if (in_array($this->status, ['active', 'ended'])) {
return false;
}
// 预热期内不可编辑
return !$this->isWithinCacheWarmupPeriod();
}🔄 状态流转
┌──────────┐ 开始时间到达 ┌──────────┐ 结束时间到达 ┌──────────┐
│ pending │ ───────────────→ │ active │ ───────────────→ │ ended │
│ 待开始 │ │ 进行中 │ │ 已结束 │
└──────────┘ └──────────┘ └──────────┘
│ │
│ 手动取消 │ 库存售罄
↓ ↓
┌──────────┐ ┌──────────┐
│ cancelled│ │ sold_out │
│ 已取消 │ │ 已售罄 │
└──────────┘ └──────────┘📦 缓存结构
Redis Key 结构:
seckill:session:{id} # 场次信息 (String/JSON)
seckill:session:{id}:products # 场次商品 (Hash, field=skuId)
seckill:stock:{sessionId} # 库存数据 (Hash, field=skuId, value=剩余库存)🕐 定时任务
系统通过定时任务自动推进活动状态:
php
// SeckillActivityStatusCrontab.php
#[Crontab(
name: 'seckill-activity-status',
rule: '* */10 * * * *', // 每10分钟执行
callback: 'execute'
)]
class SeckillActivityStatusCrontab
{
public function execute(): void
{
// 1. 处理待开始的场次(30分钟内)
$this->processPendingSessions();
// 2. 处理已过期的场次
$this->processExpiredSessions();
// 3. 联动激活活动
$this->processActivityStart();
// 4. 联动结束活动
$this->processActivityEnd();
}
}💻 API 接口
后台管理
| 接口 | 方法 | 说明 |
|---|---|---|
/admin/seckill/activity | GET | 活动列表 |
/admin/seckill/activity | POST | 创建活动 |
/admin/seckill/activity/{id} | PUT | 更新活动 |
/admin/seckill/session | GET | 场次列表 |
/admin/seckill/session | POST | 创建场次 |
/admin/seckill/product | POST | 添加商品 |
前端 API
| 接口 | 方法 | 说明 |
|---|---|---|
/api/seckill/sessions | GET | 获取进行中的场次 |
/api/seckill/products | GET | 获取场次商品列表 |
/api/order/submit | POST | 秒杀下单 |
🛡️ 防超卖策略
- Lua 原子操作: 检查和扣减在同一脚本中完成
- 分布式锁: 可选的 SKU 级别锁
- 库存预热: 活动开始前加载到 Redis
- 双重校验: 下单时再次校验库存
php
// DomainOrderStockService.php
public function reserve(array $items, string $stockHashKey): void
{
// Lua 脚本原子扣减
$result = $this->redis()->eval(self::DEDUCT_SCRIPT, $payload, 1);
if ((int) $result !== 1) {
throw new \RuntimeException('库存不足或商品已下架');
}
}📊 数据统计
- 活动销售额统计
- 场次转化率分析
- 商品热度排行
- 库存消耗趋势
⚠️ 注意事项
- 预热时间: 建议活动开始前 30 分钟完成缓存预热
- 库存同步: 活动结束后需同步 Redis 库存到数据库
- 限购控制: 支持场次级别和商品级别的限购
- 降级策略: 高并发时可启用排队机制