本文目录导读:

设计PHP缓存策略需要综合考虑应用场景、数据特征、性能要求和资源限制,一个完善的缓存体系通常包含多级缓存,从快到慢、从本地到分布式。
下面是一套完整的PHP缓存策略设计指南,涵盖策略原则、技术选型、代码实现和最佳实践。
核心原则:多级缓存架构
不要依赖单一缓存层,而要构建分层缓存体系:
浏览器缓存 → CDN缓存 → Web服务器缓存(反向代理) → 应用层缓存(内存/文件) → 数据库查询缓存
每一层都解决了不同的性能瓶颈,PHP主要工作在 应用层缓存(如Memcached/Redis)和 网页/片段缓存(如Varnish/FastCGI Cache)。
按数据类型设计缓存策略
不同的数据需要不同的缓存模式:
| 数据类型 | 推荐缓存方式 | 有效期策略 | 更新策略 |
|---|---|---|---|
| 静态页面 | 静态HTML文件 | 永久/按文件修改时间 | 生成后写入,清除陈旧文件 |
| 用户信息 | Redis Hash | 短期(5-30分钟) | 用户更新时主动删除 |
| 配置信息 | 共享内存(APCu) | 永久直到重启或更新 | 配置变更时重置 |
| 频繁查询结果 | Redis/Memcached | 依赖数据变化频率 | 写时失效或延迟失效 |
| 会话数据 | Redis | 与用户会话生命周期一致 | 每次写入同步更新 |
| 计数器/排行榜 | Redis Sorted Set/INCR | 持久化且高频写 | 写时实时更新 |
策略实现:三种核心模式
缓存旁路模式(Cache-Aside)—— 最常用
class CacheManager {
private $cache; // Redis/Memcached实例
private $ttl;
public function get($key, $fallbackCallback, $ttl = 3600) {
$data = $this->cache->get($key);
if ($data === false || $data === null) {
// 缓存未命中,从数据源获取
$data = call_user_func($fallbackCallback);
if (!empty($data)) {
$this->cache->set($key, serialize($data), $ttl);
}
} else {
$data = unserialize($data);
}
return $data;
}
// 更新数据时,同时删除缓存(延迟失效)
public function invalidate($key) {
$this->cache->del($key);
}
}
// 使用示例
$cache = new CacheManager($redisInstance);
$userData = $cache->get('user:123', function() use ($db) {
return $db->query("SELECT * FROM users WHERE id=123")->fetch();
}, 300);
读穿/写穿模式(Read/Write-Through)
class ReadThroughCache {
private $cache;
private $callback; // 查询数据库的函数
public function get($key) {
$data = $this->cache->get($key);
if ($data === false) {
$data = call_user_func($this->callback, $key);
$this->cache->set($key, $data, 3600);
}
return $data;
}
public function set($key, $value) {
// 先更新数据库
$this->updateDatabase($key, $value);
// 再更新缓存(不是删除)
$this->cache->set($key, $value, 3600);
}
}
注意:写穿模式适用于缓存数据被频繁读取且更新频率相对较低的场景。
全页面静态化(OPCache + 静态HTML + CDN)
// 生成HTML缓存文件
function generatePageCache($cacheKey, $content) {
$cacheDir = '/tmp/page_cache/';
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
$filename = $cacheDir . md5($cacheKey) . '.html';
file_put_contents($filename, $content, LOCK_EX);
// 设置过期时间(可选:通过文件修改时间判断)
}
// 检查并返回缓存
function getPageCache($cacheKey, $maxAge = 3600) {
$filename = '/tmp/page_cache/' . md5($cacheKey) . '.html';
if (file_exists($filename)) {
$age = time() - filemtime($filename);
if ($age < $maxAge) {
// 直接输出并退出(适合简单页面)
echo file_get_contents($filename);
exit;
}
// 过期则删除
unlink($filename);
}
return false;
}
缓存失效策略:避免缓存雪崩与击穿
缓存雪崩预防(大量缓存同时失效)
- 设置随机过期时间:
$ttl = 3600 + rand(0, 600); - 多级缓存:本地缓存+远程缓存(如Redis)
- 互斥锁:高并发下只让一个请求重建缓存
function getWithLock($key, $callback, $ttl = 3600) {
$data = $this->cache->get($key);
if ($data === false) {
// 获取锁
$lockKey = $key . ':lock';
if ($this->cache->setnx($lockKey, 1, 10)) { // 10秒锁超时
try {
$data = call_user_func($callback);
$this->cache->set($key, $data, $ttl);
} finally {
$this->cache->del($lockKey);
}
} else {
// 获取锁失败,等待其他线程重建
usleep(50000); // 50ms
return $this->getWithLock($key, $callback, $ttl); // 递归重试
}
}
return $data;
}
缓存穿透预防(请求不存在的数据)
- 缓存空结果:设置短TTL(如60秒)
- 布隆过滤器:在缓存前拦截不可能存在的key
- 参数校验:对于明显不合法的请求(如负数ID)直接拒绝
// 缓存空值方案
$data = $this->cache->get($key);
if ($data === false) {
$dbData = $callback();
if ($dbData === null || empty($dbData)) {
// 缓存空结果,TTL较短(防止穿透)
$this->cache->set($key, 'NULL_VALUE', 60);
} else {
$this->cache->set($key, $dbData, $ttl);
$data = $dbData;
}
} elseif ($data === 'NULL_VALUE') {
$data = null; // 表示无数据
}
缓存击穿预防(热点key过期)
- 热点数据永不过期 + 异步更新:不设置过期时间,通过后台任务更新
- 互斥锁(同上)
- 二级缓存:本地内存(如APCu)+ 远程缓存
高级技巧与PHP特定优化
使用OPcache缓存PHP中间码
; php.ini opcache.enable=1 opcache.memory_consumption=128 opcache.interned_strings_buffer=8 opcache.max_accelerated_files=10000 opcache.revalidate_freq=2 opcache.fast_shutdown=1
PHP内置缓存扩展
- APCu:适合本地缓存(变量缓存),极快
- Yac(鸟哥开发):PHP变量缓存,比APCu轻量
// APCu 示例
apcu_store('config_db', $dbConfig, 86400);
$config = apcu_fetch('config_db');
缓存键命名规范
// 使用统一前缀,方便清除
const CACHE_PREFIX = 'myapp:v1:';
function buildKey($segments) {
return CACHE_PREFIX . implode(':', array_map('strval', $segments));
}
// 批量清除模板:通过pattern匹配
// 注意:Redis KEYS命令慎用,可用SCAN
使用布隆过滤器防止缓存穿透
// 安装php扩展或通过Redis 4.0+ 模块
$redis->rawCommand('BF.RESERVE', 'user_exist', 0.01, 10000);
$redis->rawCommand('BF.ADD', 'user_exist', 'user:123');
$exists = $redis->rawCommand('BF.EXISTS', 'user_exist', 'user:456'); // 0/1
热点数据本地化(APCu + Redis 双缓存)
class HotCache {
public function get($key, $callback, $ttl = 300) {
// 第一层:APCu(本进程)
$data = apcu_fetch($key);
if ($data === false) {
// 第二层:Redis(跨进程)
$data = $this->redis->get($key);
if ($data === false) {
$data = call_user_func($callback);
$this->redis->set($key, $data, $ttl + rand(0, 30)); // 随机过期
}
apcu_store($key, $data, $ttl + rand(0, 10)); // 本地缓存时间略短于Redis
}
return $data;
}
}
监控与调优
-
命中率监控:记录缓存命中/未命中次数
if ($data === false) { $this->cache->increment('stats:miss:' . $key); // ...获取数据... } else { $this->cache->increment('stats:hit:' . $key); } -
缓存大小监控:定期检查Redis内存使用、缓存目录大小
-
慢查询与缓存效率分析:发现哪些查询经常缓存未命中
-
缓存预热:系统启动或大版本更新时,提前加载热点数据
常见避坑指南
| 问题 | 解决方案 |
|---|---|
| 缓存与数据库数据不一致 | 使用「先更新数据库,再删除缓存」模式(Cache-Aside) |
| 删除缓存失败导致脏数据 | 使用消息队列异步重试删除,或设置较短的TTL兜底 |
| 缓存过期瞬间所有请求打到底层 | 加互斥锁或使用一致性哈希避免热点重建 |
| 缓存key过多导致内存爆炸 | 设置最大内存 + LRU/LFU淘汰策略,清理过期key |
| 缓存穿透导致数据库压力 | 布隆过滤器 + 缓存空值 + 参数校验 |
| 序列化/反序列化性能损耗 | 使用igbinary或msgpack等高效序列化格式 |
通用推荐策略
- 配置信息 → APCu(永不过期,配置变更时手动重置)
- 用户会话 → Redis(持久化,TTL与session一致)
- 频繁查询/计算结果 → Redis + 本地缓存(读多写少用Cache-Aside)
- 静态页面/全页缓存 → Varnish/Nginx + PHP生成静态文件
- 热点数据 → APCu(本地) + Redis(分布式)的双层缓存
- 写操作 → 先更新数据库,再删除缓存(延迟最终一致)
缓存策略没有银弹,必须结合业务实际压测验证,建议在一个核心业务路径(如首页、用户详情页)先实施,监控命中率和性能提升后逐步推广。