Skip to content

缓存最佳实践

本节总结缓存系统在实际 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}";
    }
}

最佳实践总结

  1. 架构选择:大多数场景使用 Redis 即可;极高并发纯缓存场景考虑 Memcached
  2. 多级缓存:本地缓存(APCu)+ Redis 两级缓存提升命中率
  3. TTL 策略:所有缓存设置合理 TTL,加随机偏移避免雪崩
  4. 命名规范:使用冒号分隔的层级命名,统一管理
  5. 一致性:核心场景使用延迟双删或 Binlog 订阅,保证最终一致
  6. 监控告警:监控命中率、内存使用、连接数,设置合理告警阈值
  7. 降级策略:缓存不可用时有兜底方案,避免系统雪崩

下一节

继续学习:RabbitMQ 基础

参考链接