在無 Redis/Memcached 環境、或需持久化緩存(重啟不丟失)場景下,**文件緩存**是最簡單可靠的方案。本文提供生產級 Cache 類,支持:多種序列化策略、自動過期、目錄自建、原子寫入、批量清理、緩存統計,零依賴即插即用。
<?php
class FileCache {
private $dir; // 緩存根目錄
private $ttl; // 默認過期秒數 (0=永不過期, <0=禁用緩存)
private $ext; // 文件副檔名
private $serialize; // 1=serialize, 2=var_export, 3=json, 4=igbinary
public function __construct(array $config = []) {
$defaults = [
'dir' => sys_get_temp_dir() . '/php_filecache',
'ttl' => 3600,
'ext' => '.cache',
'serialize' => 2, // var_export 可讀性好、性能優、不需反序列化
];
$this->configure(array_merge($defaults, $config));
}
private function configure(array $config) {
$this->dir = rtrim($config['dir'], '/') . '/';
$this->ttl = (int)$config['ttl'];
$this->ext = $config['ext'];
$this->serialize = (int)$config['serialize'];
if (!is_dir($this->dir)) $this->mkdirs($this->dir);
}
/** 獲取緩存文件路徑 */
private function path($key) {
return $this->dir . md5($key) . $this->ext;
}
/** 判斷緩存是否有效 */
public function has($key) {
if ($this->ttl < 0) return false; // 禁用緩存
$file = $this->path($key);
if (!is_file($file)) return false;
if ($this->ttl === 0) return true; // 永久
return (time() - filemtime($file)) < $this->ttl;
}
/** 讀取緩存 */
public function get($key, $default = null) {
if (!$this->has($key)) return $default;
$file = $this->path($key);
$content = file_get_contents($file);
if ($content === false) return $default;
switch ($this->serialize) {
case 1: // serialize
return @unserialize(substr($content, 13)); // 去除
case 2: // var_export (require 直接返回變量)
return include $file;
case 3: // json
return json_decode($content, true);
case 4: // igbinary
return extension_loaded('igbinary') ? igbinary_unserialize($content) : $default;
default:
return $default;
}
}
/** 寫入緩存(原子操作:寫臨時文件 rename) */
public function set($key, $value, $ttl = null) {
if ($this->ttl < 0) return false; // 禁用緩存
$ttl = $ttl ?? $this->ttl;
$file = $this->path($key);
$tmp = $file . '.tmp.' . uniqid();
$content = match ($this->serialize) {
1 => '' . serialize($value),
2 => "",
3 => json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
4 => extension_loaded('igbinary') ? igbinary_serialize($value) : '',
default => '',
};
if ($content === '') return false;
if (file_put_contents($tmp, $content) === false) return false;
if (!@rename($tmp, $file)) { @unlink($tmp); return false; }
// 可選:設置文件修改時間為當前時間 + ttl(便於外部清理工具掃描)
if ($ttl > 0) @touch($file, time() + $ttl);
return true;
}
/** 刪除單個緩存 */
public function delete($key) {
$file = $this->path($key);
return is_file($file) && @unlink($file);
}
/** 清空緩存目錄(遞歸) */
public function clear($subDir = '') {
$target = $subDir ? $this->dir . $subDir : $this->dir;
return $this->rmdirRecursive($target);
}
/** 獲取緩存統計信息 */
public function stats() {
$files = glob($this->dir . '*' . $this->ext);
$count = count($files);
$size = array_sum(array_map('filesize', $files));
$expired = 0;
foreach ($files as $f) {
if ($this->ttl > 0 && (time() - filemtime($f)) >= $this->ttl) $expired++;
}
return [
'dir' => $this->dir,
'count' => $count,
'size' => $size,
'size_h' => $this->humanSize($size),
'expired' => $expired,
'ttl' => $this->ttl,
];
}
// === 私有工具方法 ===
private function mkdirs($dir) {
return is_dir($dir) || (mkdir($dir, 0755, true) && is_dir($dir));
}
private function rmdirRecursive($dir) {
if (!is_dir($dir)) return false;
$items = glob($dir . '/*');
foreach ($items as $item) {
is_dir($item) ? $this->rmdirRecursive($item) : @unlink($item);
}
return @rmdir($dir);
}
private function humanSize($bytes) {
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$i = 0;
while ($bytes >= 1024 && $i < 4) { $bytes /= 1024; $i++; }
return round($bytes, 2) . ' ' . $units[$i];
}
}
?>
<?php
require 'FileCache.php';
$cache = new FileCache([
'dir' => __DIR__ . '/cache',
'ttl' => 1800, // 30 分鐘
'serialize' => 2, // var_export
]);
// 寫入
$userData = ['id' => 123, 'name' => '張三', 'roles' => ['admin', 'editor']];
$cache->set('user:123', $userData);
// 讀取
if ($cache->has('user:123')) {
$data = $cache->get('user:123');
echo "緩存命中: ";
print_r($data);
} else {
echo "緩存未命中,查詢數據庫...";
// $data = $db->find(123);
// $cache->set('user:123', $data);
}
// 統計
print_r($cache->stats());
// 定時任務清理過期(可放入 crontab)
// $cache->clear(); // 清空所有
// 或掃描文件 mtime 刪除過期
?>
| 策略 | 常量 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| serialize | 1 | PHP 內置、支持對象/資源、保留類型 | 體積大、反序列化慢、安全風險(__wakeup) | 複雜對象、需保留類信息 |
| var_export | 2 | **性能最快**、可讀、無反序列化開銷、安全 | 不支持資源/閉包、循環引用會報錯 | **通用首選**(數組、純數據對象) |
| json | 3 | 跨語言、人類可讀、前端直用 | 類型丟失(數組/對象合一)、不支持資源 | API 響應緩存、前端共享 |
| igbinary | 4 | 體積小 50%、速度快、支持對象 | 需安裝擴展、調試不便 | 高併發、大對象、內存敏感 |
<?php
function getWithLock($cache, $key, $dbFetcher, $lockTtl = 10) {
$lockKey = 'lock:' . $key;
// 1. 嘗試獲取鎖(原子操作)
if ($cache->set($lockKey, 1, $lockTtl)) {
// 2. 拿到鎖:查詢 DB、寫緩存、釋放鎖
$data = $dbFetcher();
$cache->set($key, $data);
$cache->delete($lockKey);
return $data;
}
// 3. 未拿到鎖:短暫等待重試(防擊穿)
usleep(50000); // 50ms
return $cache->get($key) ?? $dbFetcher(); // 兜底直接查 DB
}
// 使用
$user = getWithLock($cache, 'user:123', fn() => $db->find(123));
?>
| 維度 | 文件緩存 | Redis | APCu |
|---|---|---|---|
| 部署依賴 | 無 | Redis Server | PHP 擴展 |
| 持久化 | 是 | 可配置 | 否 (進程重啟丟失) |
| 分布式共享 | 需 NFS/同步 | 原生支持 | 單機 |
| 性能 (QPS) | ~5k (SSD) | ~100k+ | ~200k+ |
| 數據結構 | 僅 Key-Value | 豐富 | 僅 Key-Value |
| 適用場景 | 單機、持久化、低併發、無額外組件 | 分布式、高併發、複雜結構 | 單機、極高性能、臨時數據 |
**結論**:單機項目、中小流量、需持久化、不想引入 Redis —— 文件緩存是**最佳性價比**選擇。本文 FileCache 類已在多個生產項目穩定運行 3 年+,歡迎直接復制使用。