在PHP应用里,当数据库查询变慢或重复计算变多时,引入缓存往往是最直接的优化手段。缓存的核心思想是把不易变或计算成本高的数据暂存起来,下次请求直接读取,从而减少对后端资源的压力。PHP生态下最常见的两类缓存是文件缓存与内存缓存,它们底层机制不同,适用场景也有明显分界。

文件缓存的实现原理与基础用法
文件缓存是指将序列化后的数据写入服务器磁盘的某个目录中,通常以键值对应文件名,内容存放序列化结果。它的优势在于几乎不依赖额外服务,只要PHP有写权限的目录即可工作,特别适合单机部署、少量数据且更新不频繁的场景,例如站点配置、HTML片段、接口字典等。
使用文件缓存时要重点考虑并发读写安全与失效机制。最简单的方式是以时间戳记录写入时间,读取时判断文件修改时间是否超过有效期;更严谨的做法是引入文件锁flock避免同时写入导致内容损坏。下面示例展示一个基础的文件缓存类:
<?php
class FileCache {
private $dir;
public function __construct($dir) {
$this->dir = rtrim($dir, '/');
if (!is_dir($this->dir)) {
mkdir($this->dir, 0755, true);
}
}
// 写入缓存,过期时间单位秒
public function set($key, $data, $ttl = 3600) {
$file = $this->dir . '/' . md5($key) . '.cache';
$content = serialize(array(
'expire' => time() + $ttl,
'data' => $data
));
$fp = fopen($file, 'w');
if (flock($fp, LOCK_EX)) {
fwrite($fp, $content);
flock($fp, LOCK_UN);
}
fclose($fp);
}
// 读取缓存,失效返回false
public function get($key) {
$file = $this->dir . '/' . md5($key) . '.cache';
if (!file_exists($file)) {
return false;
}
$content = file_get_contents($file);
$arr = unserialize($content);
if ($arr['expire'] < time()) {
unlink($file);
return false;
}
return $arr['data'];
}
}
// 使用示例
$cache = new FileCache('/tmp/php_cache');
$cache->set('user_1', array('name' => '张三', 'age' => 20), 600);
$user = $cache->get('user_1');
var_dump($user);
?>
上述代码通过md5将键名映射为文件名,避免特殊字符引发路径错误。序列化使用serialize与unserialize,可以保存数组与对象。但文件缓存的缺点也很明显:每次读写都涉及磁盘IO,在每秒数千次请求下磁盘会成为瓶颈;且多台服务器间文件不共享,难以支撑集群。
此外,文件缓存要注意清理策略。若只写不删,目录文件数会膨胀,导致遍历变慢。可以配合定时脚本扫描过期文件,或在set时随机触发删除逻辑。对于Windows服务器,路径应写成C:ASRphp_cache这类带反斜杠的形式,但代码中需用正斜杠或转义反斜杠保证解析正确。
内存缓存的工作机制与Redis、APCu对比
内存缓存将数据放在服务器内存中,由专门进程管理,读写延时在微秒级。PHP常用方案有Redis与APCu。Redis是独立服务,支持网络访问、持久化、丰富数据结构,适合分布式与共享缓存;APCu是PHP进程内缓存,数据仅在当前PHP-FPM实例中有效,无法跨机但访问极快。
在代码层面,Redis通过扩展提供connect与setex等函数操作。以下示例演示用Redis保存用户数据并设置过期:
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->auth('your_password');
$key = 'user:1';
if (!$redis->exists($key)) {
// 模拟从数据库读取
$dbData = array('name' => '李四', 'score' => 88);
$redis->setex($key, 300, serialize($dbData));
} else {
$dbData = unserialize($redis->get($key));
}
print_r($dbData);
?>
APCu则更简单,无需连接服务,直接调用apcu_store与apcu_fetch。它适合缓存编译结果、路由表等单机临时数据。示例:
<?php
if (!apcu_exists('config')) {
$config = array('timeout' => 30, 'debug' => false);
apcu_store('config', $config, 600);
} else {
$config = apcu_fetch('config');
}
var_dump($config);
?>
两者对比看,Redis支持主从复制与集群,当应用部署在多台机器时必须用Redis统一缓存来源;APCu因数据不共享,在负载均衡下可能出现各节点缓存不一致。但APCu免去网络开销,在单节点高并发API中表现优异。选择时应评估是否需跨进程、是否允许丢失、数据体积等指标。
缓存策略设计与常见坑点规避
无论文件还是内存缓存,都要设计合理的键命名与失效时间。键建议采用业务前缀加主键,如order:detail:1001,避免冲突。失效时间不要统一设相同值,否则大量缓存同时过期会引发雪崩,可给基础TTL加随机偏移,例如300 + mt_rand(0, 60)。
另一个坑是缓存穿透:查询不存在的数据反复打向数据库。解决方案是对空结果也缓存短时间的标记,或使用布隆过滤器拦截。缓存击穿指热点键失效瞬间高并发重建,可用互斥锁控制只一个请求回源,其余等待。文件缓存可用flock模拟锁,Redis可用setnx实现。
最后需关注序列化方式。默认serialize兼容性好但速度一般;若用Redis可考虑igbinary扩展提升效率。对于大对象,缓存前应先提取必要字段,减小体积。当数据频繁变改且一致性要求极高时,缓存带来的复杂度可能超过收益,此时应优先优化数据库索引而非强行加缓存。