缓存最佳实践
本节总结缓存系统在实际 PHP 项目中的最佳实践,涵盖多级缓存架构设计、缓存预热策略、缓存一致性方案、监控与告警体系等核心内容,帮助团队构建高可用、高性能的缓存基础设施。
前置知识
阅读本节前,建议先了解:缓存策略概览 和 Memcached PHP 客户端
多级缓存架构
两级缓存设计
请求 → 浏览器缓存 → CDN → Nginx 缓存 → PHP 本地缓存 → Redis 缓存 → 数据库php
<?php
declare(strict_types=1);
namespace App\Cache;
use Redis;
use Psr\SimpleCache\CacheInterface;
/**
* 多级缓存管理器
* L1: 本地缓存(APCu / 进程内数组)
* L2: 分布式缓存(Redis)
*/
class MultiLevelCache implements CacheInterface
{
private int $localTTL;
private int $remoteTTL;
public function __construct(
private readonly Redis $redis,
private readonly string $prefix = 'mlc:',
int $localTTL = 60,
int $remoteTTL = 3600
) {
$this->localTTL = $localTTL;
$this->remoteTTL = $remoteTTL;
}
/**
* 多级缓存读取
* L1 → L2 → 数据源
*/
public function get(string $key, mixed $default = null): mixed
{
$fullKey = $this->prefix . $key;
// L1: 本地缓存(APCu)
$localKey = "local:{$fullKey}";
$localData = apcu_fetch($localKey, $success);
if ($success) {
return $localData;
}
// L2: Redis 分布式缓存
try {
$remoteData = $this->redis->get($fullKey);
if ($remoteData !== false) {
$data = unserialize($remoteData);
// 回填本地缓存
apcu_store($localKey, $data, $this->localTTL);
return $data;
}
} catch (\Throwable $e) {
// Redis 异常,降级到本地缓存或返回默认值
}
return $default;
}
/**
* 多级缓存写入
* 同时写入 L1 和 L2
*/
public function set(string $key, mixed $value, ?int $ttl = null): bool
{
$fullKey = $this->prefix . $key;
$localKey = "local:{$fullKey}";
$remoteTTL = $ttl ?? $this->remoteTTL;
// 写入本地缓存
apcu_store($localKey, $value, $this->localTTL);
// 写入 Redis
try {
return $this->redis->setex($fullKey, $remoteTTL, serialize($value));
} catch (\Throwable $e) {
return true; // Redis 写入失败不影响本地缓存
}
}
public function delete(string $key): bool
{
$fullKey = $this->prefix . $key;
$localKey = "local:{$fullKey}";
apcu_delete($localKey);
try {
return (bool) $this->redis->del($fullKey);
} catch (\Throwable $e) {
return true;
}
}
public function clear(): bool
{
apcu_clear_cache();
try {
$this->redis->flushDB();
} catch (\Throwable $e) {
// ignore
}
return true;
}
public function has(string $key): bool
{
return $this->get($key) !== null;
}
public function getMultiple(iterable $keys, mixed $default = null): iterable
{
$result = [];
foreach ($keys as $key) {
$result[$key] = $this->get($key, $default);
}
return $result;
}
public function setMultiple(iterable $values, ?int $ttl = null): bool
{
$success = true;
foreach ($values as $key => $value) {
if (!$this->set($key, $value, $ttl)) {
$success = false;
}
}
return $success;
}
public function deleteMultiple(iterable $keys): bool
{
$success = true;
foreach ($keys as $key) {
if (!$this->delete($key)) {
$success = false;
}
}
return $success;
}
}进程内缓存(适用于 PHP-FPM Worker)
php
<?php
declare(strict_types=1);
namespace App\Cache;
/**
* PHP-FPM Worker 级别的本地缓存
* 在同一个 Worker 进程内有效
* Worker 重启后自动清空
*/
class ProcessLocalCache
{
private static array $cache = [];
private static array $expiry = [];
/**
* 获取数据
*/
public static function get(string $key): mixed
{
if (!isset(self::$cache[$key])) {
return null;
}
// 检查是否过期
if (isset(self::$expiry[$key]) && self::$expiry[$key] < time()) {
unset(self::$cache[$key], self::$expiry[$key]);
return null;
}
return self::$cache[$key];
}
/**
* 设置数据
*/
public static function set(string $key, mixed $value, int $ttl = 0): void
{
self::$cache[$key] = $value;
if ($ttl > 0) {
self::$expiry[$key] = time() + $ttl;
}
}
/**
* 删除数据
*/
public static function delete(string $key): void
{
unset(self::$cache[$key], self::$expiry[$key]);
}
/**
* 清空缓存
*/
public static function clear(): void
{
self::$cache = [];
self::$expiry = [];
}
}缓存预热
什么是缓存预热
缓存预热(Cache Warm-up)是指在系统启动或大流量来临之前,提前将热点数据加载到缓存中,避免冷启动时大量缓存未命中导致的数据库压力。
php
<?php
declare(strict_types=1);
namespace App\Service;
use App\Cache\MultiLevelCache;
use App\Repository\ProductRepository;
class CacheWarmupService
{
public function __construct(
private readonly MultiLevelCache $cache,
private readonly ProductRepository $productRepo
) {}
/**
* 预热热门商品缓存
*/
public function warmupHotProducts(int $limit = 1000): array
{
$stats = ['loaded' => 0, 'errors' => 0];
$startTime = microtime(true);
// 获取热门商品列表
$products = $this->productRepo->getHotProducts($limit);
foreach ($products as $product) {
try {
$this->cache->set(
"product:detail:{$product['id']}",
$product,
3600 // 1 小时 TTL
);
$stats['loaded']++;
} catch (\Throwable $e) {
$stats['errors']++;
}
}
$stats['duration'] = round(microtime(true) - $startTime, 3);
return $stats;
}
/**
* 预热分类缓存
*/
public function warmupCategories(): array
{
$categories = $this->productRepo->getAllCategories();
$this->cache->set('categories:tree', $categories, 7200);
return ['loaded' => count($categories)];
}
/**
* 批量预热(使用队列逐步处理)
*/
public function warmupByQueue(): void
{
$productIds = $this->productRepo->getAllIds();
foreach (array_chunk($productIds, 500) as $chunk) {
// 投递到预热队列
// Queue::push(WarmupJob::class, ['ids' => $chunk]);
}
}
}定时预热脚本
php
<?php
declare(strict_types=1);
// cache_warmup.php — CLI 脚本,由 Cron 定时执行
// crontab: */5 * * * * php /path/to/cache_warmup.php
require __DIR__ . '/vendor/autoload.php';
use App\Service\CacheWarmupService;
use App\Cache\MultiLevelCache;
$cache = new MultiLevelCache(createRedisConnection());
$service = new CacheWarmupService($cache, createProductRepository());
// 执行预热
echo "Starting cache warmup at " . date('Y-m-d H:i:s') . "\n";
$result = $service->warmupHotProducts(500);
echo "Hot products: {$result['loaded']} loaded, {$result['errors']} errors\n";
$result = $service->warmupCategories();
echo "Categories: {$result['loaded']} loaded\n";
echo "Warmup completed\n";缓存一致性方案
基于 Binlog 的缓存同步
php
<?php
declare(strict_types=1);
namespace App\Listener;
use Redis;
/**
* 数据库变更监听器(通过消息队列接收变更通知)
* 实现缓存与数据库的最终一致性
*/
class DatabaseChangeListener
{
public function __construct(
private readonly Redis $redis
) {}
/**
* 处理数据变更事件
*
* 事件格式:
* {
* "table": "products",
* "action": "update",
* "id": 1001,
* "data": { "title": "新标题" }
* }
*/
public function handleDatabaseChange(array $event): void
{
$table = $event['table'];
$action = $event['action'];
$id = $event['id'];
$cacheKey = $this->getCacheKey($table, $id);
match ($action) {
'insert' => $this->handleInsert($cacheKey, $event['data'] ?? []),
'update' => $this->handleUpdate($cacheKey, $event['data'] ?? []),
'delete' => $this->handleDelete($cacheKey),
default => null,
};
}
private function handleInsert(string $cacheKey, array $data): void
{
// 新增数据,直接写入缓存
$this->redis->setex($cacheKey, 3600, json_encode($data));
}
private function handleUpdate(string $cacheKey, array $data): void
{
// 更新数据,删除缓存(下次读取时重新加载)
$this->redis->del($cacheKey);
// 也可以直接更新缓存(如果数据完整)
// $this->redis->setex($cacheKey, 3600, json_encode($data));
}
private function handleDelete(string $cacheKey): void
{
// 删除数据,删除缓存
$this->redis->del($cacheKey);
}
/**
* 批量失效(清除关联缓存)
*/
public function invalidateRelated(string $table, array $ids): void
{
$pipeline = $this->redis->pipeline();
foreach ($ids as $id) {
$cacheKey = $this->getCacheKey($table, $id);
$pipeline->del($cacheKey);
}
$pipeline->exec();
}
private function getCacheKey(string $table, int|string $id): string
{
return "cache:{$table}:{$id}";
}
}双写一致性保障
php
<?php
declare(strict_types=1);
namespace App\Service;
use Redis;
class CacheConsistencyService
{
public function __construct(
private readonly Redis $redis
) {}
/**
* 方案一:延迟双删(推荐,兼顾性能和一致性)
* 流程:先删缓存 → 更新DB → 延迟N毫秒 → 再删缓存
*/
public function delayedDoubleDelete(string $cacheKey, callable $dbUpdate, int $delayMs = 500): bool
{
// 第 1 次删除缓存
$this->redis->del($cacheKey);
// 更新数据库
$result = $dbUpdate();
if (!$result) {
return false;
}
// 延迟后第 2 次删除缓存(消除并发写入导致的不一致)
usleep($delayMs * 1000);
$this->redis->del($cacheKey);
return true;
}
/**
* 方案二:消息队列异步删除
* 流程:先删缓存 → 更新DB → 发送删除消息 → 消费者删除缓存
*/
public function mqDelete(string $cacheKey, callable $dbUpdate): bool
{
// 第 1 次删除缓存
$this->redis->del($cacheKey);
// 更新数据库
$result = $dbUpdate();
if (!$result) {
return false;
}
// 发送删除消息到队列
// MessageQueue::publish('cache_invalidate', ['key' => $cacheKey]);
return true;
}
}缓存监控与告警
监控指标
php
<?php
declare(strict_types=1);
namespace App\Service;
use Redis;
class CacheMonitorService
{
public function __construct(
private readonly Redis $redis
) {}
/**
* 收集缓存监控指标
*/
public function collectMetrics(): array
{
$info = $this->redis->info();
$stats = $this->redis->info('stats');
$memory = $this->redis->info('memory');
return [
'basic' => [
'version' => $info['redis_version'] ?? 'unknown',
'uptime_days' => (int) (($info['uptime_in_seconds'] ?? 0) / 86400),
'connected_clients' => (int) ($info['connected_clients'] ?? 0),
'used_memory_human' => $memory['used_memory_human'] ?? '0B',
'used_memory_peak_human' => $memory['used_memory_peak_human'] ?? '0B',
'maxmemory_human' => $memory['maxmemory_human'] ?? 'unlimited',
'mem_fragmentation_ratio' => round(
(float) ($memory['used_memory_rss'] ?? 0) / max(1, (float) ($memory['used_memory'] ?? 1)),
2
),
],
'performance' => [
'keyspace_hits' => (int) ($stats['keyspace_hits'] ?? 0),
'keyspace_misses' => (int) ($stats['keyspace_misses'] ?? 0),
'hit_rate' => $this->calculateHitRate($stats),
'ops_per_sec' => (int) ($info['instantaneous_ops_per_sec'] ?? 0),
'total_commands_processed' => (int) ($stats['total_commands_processed'] ?? 0),
],
'latency' => [
'latest_fork_usec' => (int) ($info['latest_fork_usec'] ?? 0),
],
];
}
/**
* 计算命中率
*/
private function calculateHitRate(array $stats): float
{
$hits = (int) ($stats['keyspace_hits'] ?? 0);
$misses = (int) ($stats['keyspace_misses'] ?? 0);
$total = $hits + $misses;
return $total === 0 ? 0.0 : round($hits / $total * 100, 2);
}
/**
* 健康检查
*/
public function healthCheck(): array
{
try {
$this->redis->ping();
$metrics = $this->collectMetrics();
$hitRate = $metrics['performance']['hit_rate'];
$fragmentation = $metrics['basic']['mem_fragmentation_ratio'];
$opsPerSec = $metrics['performance']['ops_per_sec'];
$warnings = [];
if ($hitRate < 80) {
$warnings[] = "低命中率: {$hitRate}%";
}
if ($fragmentation > 1.5) {
$warnings[] = "高内存碎片率: {$fragmentation}";
}
return [
'status' => 'healthy',
'warnings' => $warnings,
'metrics' => $metrics,
];
} catch (\Throwable $e) {
return [
'status' => 'unhealthy',
'error' => $e->getMessage(),
];
}
}
}告警规则示例
yaml
# Prometheus AlertManager 告警规则示例
groups:
- name: cache_alerts
rules:
- alert: CacheHitRateLow
expr: redis_cache_hit_rate < 80
for: 5m
labels:
severity: warning
annotations:
summary: "缓存命中率低于 80%"
description: "当前命中率: {{ $value }}%"
- alert: RedisMemoryHigh
expr: redis_used_memory_percentage > 85
for: 3m
labels:
severity: critical
annotations:
summary: "Redis 内存使用率过高"
- alert: RedisHighFragmentation
expr: redis_mem_fragmentation_ratio > 1.5
for: 10m
labels:
severity: warning
annotations:
summary: "Redis 内存碎片率过高"注意事项
缓存 Key 设计规范
php
<?php
namespace App\Constants;
class CacheKeyConstants
{
// Key 命名格式:{业务}:{实体}:{ID}:{属性}
// 示例:product:detail:1001
// user:profile:1001
// category:tree
// 业务前缀
public const PREFIX_PRODUCT = 'product:';
public const PREFIX_USER = 'user:';
public const PREFIX_ORDER = 'order:';
public const PREFIX_SESSION = 'session:';
public const PREFIX_RATE = 'rate:';
// 缓存过期时间(秒)
public const TTL_SHORT = 300; // 5 分钟
public const TTL_MEDIUM = 3600; // 1 小时
public const TTL_LONG = 86400; // 24 小时
public const TTL_PERMANENT = 0; // 永不过期(不推荐)
/**
* 生成商品详情缓存 Key
*/
public static function productDetail(int $productId): string
{
return self::PREFIX_PRODUCT . "detail:{$productId}";
}
/**
* 生成用户信息缓存 Key
*/
public static function userProfile(int $userId): string
{
return self::PREFIX_USER . "profile:{$userId}";
}
/**
* 生成限流器 Key
*/
public static function rateLimiter(string $identifier, string $action): string
{
return self::PREFIX_RATE . "{$action}:{$identifier}";
}
}最佳实践总结
- 架构选择:大多数场景使用 Redis 即可;极高并发纯缓存场景考虑 Memcached
- 多级缓存:本地缓存(APCu)+ Redis 两级缓存提升命中率
- TTL 策略:所有缓存设置合理 TTL,加随机偏移避免雪崩
- 命名规范:使用冒号分隔的层级命名,统一管理
- 一致性:核心场景使用延迟双删或 Binlog 订阅,保证最终一致
- 监控告警:监控命中率、内存使用、连接数,设置合理告警阈值
- 降级策略:缓存不可用时有兜底方案,避免系统雪崩
下一节
继续学习:RabbitMQ 基础