include 直接回傳陣列/常數,效能極高<?php
$arr = ['name' => 'John', 'tags' => ['php', 'cache']];
// 1. var_export 產生合法 PHP 代碼字串
$code = '';
// 產生: 'John', 'tags' => array (0 => 'php', 1 => 'cache'),); ?>
// 2. 寫入檔案
file_put_contents('cache/user_123.php', $code);
// 3. 讀取時直接 include,回傳陣列
$data = include 'cache/user_123.php';
// $data 即為原始陣列,無需 unserialize/json_decode
優勢:OPcache 會編譯快取檔案,後續讀取極快;比 JSON/unserialize 少一次解析步驟。
<?php
class FileCache {
private string $dir;
private int $defaultTtl;
public function __construct(string $dir = __DIR__ . '/cache', int $defaultTtl = 3600) {
$this->dir = rtrim($dir, '/') . '/';
$this->defaultTtl = $defaultTtl;
if (!is_dir($this->dir)) mkdir($this->dir, 0755, true);
}
// 寫入快取 (支援陣列、常數陣列)
public function write(string $key, mixed $data, int $ttl = 0): bool {
$ttl = $ttl ?: $this->defaultTtl;
$file = $this->dir . $this->sanitizeKey($key) . '.php';
$content = $this->serializeData($data, $ttl);
$bytes = @file_put_contents($file, $content, LOCK_EX);
if ($bytes === false) return false;
@chmod($file, 0644);
return true;
}
// 讀取快取 (自動檢查過期)
public function read(string $key, mixed $default = null): mixed {
$file = $this->dir . $this->sanitizeKey($key) . '.php';
if (!file_exists($file)) return $default;
// 檢查過期 (檔案修改時間 + TTL)
$metaFile = $file . '.meta';
if (file_exists($metaFile)) {
$meta = json_decode(file_get_contents($metaFile), true);
if ($meta && (time() > $meta['expire'])) {
$this->delete($key);
return $default;
}
}
// 直接 include 回傳資料
return @include $file ?: $default;
}
// 刪除快取
public function delete(string $key): bool {
$file = $this->dir . $this->sanitizeKey($key) . '.php';
$metaFile = $file . '.meta';
$ok1 = @unlink($file);
$ok2 = @unlink($metaFile);
return $ok1 || $ok2;
}
// 清空所有快取
public function clear(): int {
$count = 0;
foreach (glob($this->dir . '*.php') as $file) {
if (@unlink($file)) $count++;
@unlink($file . '.meta');
}
return $count;
}
// 取得快取資訊
public function info(string $key): ?array {
$file = $this->dir . $this->sanitizeKey($key) . '.php';
if (!file_exists($file)) return null;
$metaFile = $file . '.meta';
$meta = file_exists($metaFile) ? json_decode(file_get_contents($metaFile), true) : [];
return [
'key' => $key,
'size' => filesize($file),
'mtime' => filemtime($file),
'expire' => $meta['expire'] ?? null,
'ttl' => $meta['ttl'] ?? null,
];
}
private function sanitizeKey(string $key): string {
return preg_replace('/[^a-zA-Z0-9_-]/', '_', $key);
}
private function serializeData(mixed $data, int $ttl): string {
$expire = time() + $ttl;
// 寫入中繼資料 (過期時間)
$metaFile = $this->dir . $this->sanitizeKey($key) . '.php.meta';
file_put_contents($metaFile, json_encode(['expire' => $expire, 'ttl' => $ttl]));
// 區分陣列類型輸出
if (is_array($data)) {
$keys = array_keys($data);
$isAssoc = $keys !== range(0, count($keys) - 1);
if ($isAssoc) {
// 關聯陣列:輸出 define() 常數陣列 (可選)
$code = " $v) {
$code .= "define('" . strtoupper(preg_replace('/[^A-Z0-9_]/', '', $k)) . "', " . var_export($v, true) . ");\n";
}
$code .= "?>";
} else {
// 索引陣列:直接 return 陣列
$code = "";
}
} else {
// 純量值
$code = "";
}
return $code;
}
}
<?php
require 'FileCache.php';
$cache = new FileCache(__DIR__ . '/cache', 1800); // 30分鐘預設 TTL
// 寫入
$userData = [
'id' => 123,
'name' => 'John Doe',
'roles' => ['admin', 'editor'],
'prefs' => ['theme' => 'dark', 'lang' => 'zh-TW']
];
$cache->write('user_123', $userData, 3600);
// 讀取
$user = $cache->read('user_123', []); // 不存在回傳空陣列
if ($user) {
echo "用戶: {$user['name']}, 角色: " . implode(', ', $user['roles']);
}
// 條件式快取模式
function getExpensiveData($id, FileCache $cache) {
$key = "expensive_data_$id";
$data = $cache->read($key);
if ($data === null) { // 快取未命中
// 模擬昂貴操作
sleep(2);
$data = ['result' => 'computed_' . $id, 'time' => time()];
$cache->write($key, $data, 600); // 快取 10 分鐘
}
return $data;
}
a = 本機讀寫時間 (include + OPcache)
b = 本機佔用空間
c = 網路傳輸時間 (到 DB)
d = 資料庫服務器磁盤/CPU 時間
快取有效當:a + b < c + d
典型數值 (單次查詢):
- a: 0.1-0.5 ms (OPcache 後)
- c: 0.5-2 ms (本機網路)
- d: 1-10 ms (查詢執行)
結論:單機部署、查詢複雜、頻率高時效果明顯。
分散式部署時 c 變大,快取更有價值;但檔案快取需共享儲存 (NFS/GlusterFS) 或改用 Redis。
<?php
// 寫入時關聯標籤
$cache->write('article_1', $data, 3600, ['tag' => 'article', 'tag' => 'user_123']);
// 刪除所有 user_123 相關快取
$cache->deleteByTag('user_123'); // 需額外索引機制,檔案快取較難實現,建議用 Redis
<?php
function getWithLock(string $key, callable $fallback, FileCache $cache, int $ttl = 3600) {
$data = $cache->read($key);
if ($data !== null) return $data;
// 簡易鎖 (檔案鎖)
$lockFile = $cache->getDir() . ".lock_{$key}";
$fp = fopen($lockFile, 'w');
if (flock($fp, LOCK_EX | LOCK_NB)) {
try {
$data = $fallback(); // 只有持鎖者執行昂貴操作
$cache->write($key, $data, $ttl);
} finally {
flock($fp, LOCK_UN);
fclose($fp);
@unlink($lockFile);
}
} else {
// 等待持鎖者完成
usleep(100000); // 100ms
return $cache->read($key, $fallback());
}
return $data;
}
<?php
// 部署後/清空快取後,背景腳本預熱
foreach (getHotKeys() as $key) {
$data = computeExpensive($key);
$cache->write($key, $data, 3600);
}
| 坑 | 後果 | 解決 |
|---|---|---|
| 快取檔案無副檔名/.php | Web 可直接訪問洩露資料 | 目錄設 deny from all、或放在 Webroot 外 |
| var_export 循環引用 | Fatal error / 無限遞迴 | 資料結構扁平化、移除循環引用 |
| 大量小檔案 inode 耗盡 | 無法建立新檔案 | 合併快取、定期清理、用 Redis |
| 併發寫入損壞檔案 | include 解析錯誤 | LOCK_EX + 原子寫入 (tmp rename) |
| TTL 未檢查直接讀取 | 髒讀過期資料 | read() 內建檢查過期 |