Appearance
缓存预热
MineShop 在秒杀、团购等高并发场景中使用缓存预热机制,确保活动开始时热点数据已在 Redis 中就绪。
🎯 预热机制
┌─────────────────────────────────────────────────────────────┐
│ 活动时间线 │
├─────────────────────────────────────────────────────────────┤
│ │
│ T-30min T-0 T+N T+End │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ 预热 │ │ 开始 │ │ 进行 │ │ 结束 │ │
│ │ 缓存 │ │ 活动 │ │ 中 │ │ 清理 │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ │
│ │
│ • 加载数据 • 状态激活 • 处理请求 • 清理缓存 │
│ • 禁止编辑 • 开放购买 • 扣减库存 • 同步数据 │
│ │
└─────────────────────────────────────────────────────────────┘✨ 秒杀缓存预热
缓存结构
Redis Key 结构:
seckill:session:{id} # 场次信息 (String/JSON)
seckill:session:{id}:products # 场次商品 (Hash, field=skuId)
seckill:stock:{sessionId} # 库存数据 (Hash, field=skuId, value=剩余库存)预热服务
php
// SeckillCacheService.php
final class SeckillCacheService
{
private const PREFIX = 'seckill';
private const TTL = 7200; // 2小时
/**
* 预热场次缓存
*/
public function warmSession(int $sessionId): void
{
$model = $this->sessionRepository->findById($sessionId);
if (!$model) {
return;
}
// 1. 缓存场次信息
$this->cache->set(
$this->sessionKey($sessionId),
json_encode($model->toArray(), JSON_UNESCAPED_UNICODE),
['EX' => self::TTL]
);
// 2. 缓存商品列表
$products = $this->productRepository->findBySessionId($sessionId);
$productsKey = $this->sessionProductsKey($sessionId);
$this->cache->delete($productsKey);
$fields = [];
foreach ($products as $product) {
$fields[(string) $product->product_sku_id] = json_encode(
SeckillProductMapper::fromModel($product)->toArray(),
JSON_UNESCAPED_UNICODE
);
}
if ($fields !== []) {
$this->cache->hMset($productsKey, $fields);
}
// 3. 预热库存
$this->warmStockHash($sessionId, $products);
}
/**
* 预热库存 Hash
*/
private function warmStockHash(int $sessionId, array $products): void
{
$hashKey = sprintf('stock:%d', $sessionId);
$stockFields = [];
foreach ($products as $product) {
if (!$product->is_enabled) {
continue;
}
$remaining = max(0, $product->quantity - $product->sold_quantity);
$stockFields[(string) $product->product_sku_id] = (string) $remaining;
}
$this->cache->delete($hashKey);
if ($stockFields !== []) {
$this->cache->hMset($hashKey, $stockFields);
}
}
}✨ 团购缓存预热
php
// GroupBuyCacheService.php
final class GroupBuyCacheService
{
private const PREFIX = 'groupbuy';
public function warmStock(int $groupBuyId): void
{
$model = $this->repository->findById($groupBuyId);
if (!$model) {
return;
}
// 计算剩余库存
$remaining = max(0, $model->total_quantity - $model->sold_quantity);
$skuId = (int) $model->sku_id;
// 写入 Redis Hash
$hashKey = sprintf('stock:%d', $groupBuyId);
$this->cache->delete($hashKey);
$this->cache->hMset($hashKey, [(string) $skuId => (string) $remaining]);
}
public function evictStock(int $groupBuyId): void
{
$this->cache->delete(sprintf('stock:%d', $groupBuyId));
}
}🕐 定时预热
秒杀定时任务
php
// SeckillActivityStatusCrontab.php
#[Crontab(name: 'seckill-activity-status', rule: '* */10 * * * *')]
class SeckillActivityStatusCrontab
{
private function processPendingSessions(): void
{
// 查找 30 分钟内即将开始的场次
$sessions = $this->sessionRepository->findPendingSessionsWithinMinutes(30);
foreach ($sessions as $session) {
$startTime = Carbon::parse($session->start_time);
if ($startTime->lte(Carbon::now())) {
// 已到开始时间,立即激活并预热
$this->sessionService->start($session->id);
$this->cacheService->warmSession($session->id);
} else {
// 未到开始时间,投递延迟 Job
$delaySeconds = $startTime->diffInSeconds(Carbon::now());
$this->driverFactory->get('default')->push(
new SeckillSessionStartJob($session->id, $session->activity_id),
$delaySeconds
);
}
}
}
}团购定时任务
php
// GroupBuyActivityStatusCrontab.php
#[Crontab(name: 'group-buy-activity-status', rule: '*/10 * * * *')]
class GroupBuyActivityStatusCrontab
{
private function processPendingActivities(): void
{
$activities = $this->repository->findPendingActivitiesWithinMinutes(30);
foreach ($activities as $activity) {
$startTime = Carbon::parse($activity->start_time);
if ($startTime->lte(Carbon::now())) {
// 立即激活并预热
$this->groupBuyService->start($activity->id);
$this->cacheService->warmStock($activity->id);
} else {
// 延迟 Job
$delaySeconds = $startTime->diffInSeconds(Carbon::now());
$this->driverFactory->get('default')->push(
new GroupBuyStartJob($activity->id),
$delaySeconds
);
}
}
}
}🔒 编辑锁定
预热期间禁止编辑活动,防止缓存与数据库不一致:
php
// SeckillSessionEntity.php
public function isWithinCacheWarmupPeriod(): bool
{
if (empty($this->startTime)) {
return false;
}
$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', 'sold_out'])) {
return false;
}
// 预热期内不可编辑
return !$this->isWithinCacheWarmupPeriod();
}📊 缓存读取
php
// SeckillCacheService.php
public function getSession(int $sessionId): ?SeckillSessionEntity
{
// 优先从缓存读取
$json = $this->cache->get($this->sessionKey($sessionId));
if (is_string($json) && $json !== '') {
$data = json_decode($json, true);
if (is_array($data)) {
return $this->hydrateSession($data);
}
}
// 缓存未命中,从数据库加载并预热
$model = $this->sessionRepository->findById($sessionId);
if (!$model) {
return null;
}
$this->warmSession($sessionId);
return SeckillSessionMapper::fromModel($model);
}⚠️ 注意事项
- 预热时机: 活动开始前 30 分钟自动预热
- 数据一致性: 预热期间禁止编辑,避免缓存不一致
- 缓存过期: 设置合理的 TTL,活动结束后自动过期
- 失败重试: 预热失败时定时任务会兜底重试
- 清理机制: 活动结束后及时清理缓存