在無法安裝 Redis/APCu 的虛擬主機環境,或需快速為特定頁面加速時,基於檔案修改時間 (filemtime) 的快取類別是極佳選擇。本文實作一個簡潔、可直接使用的 PageCache 類別,支援自動過期、直接輸出或回傳內容兩種模式。
<?php
class PageCache {
/** @var string 快取檔案完整路徑 */
public $file;
/** @var int 快存有效秒數,預設 1 小時 */
public $cacheTime = 3600;
/**
* 建構子
* @param string $file 快取檔案路徑
* @param int $cacheTime 快取秒數
*/
public function __construct($file, $cacheTime = 3600) {
$this->file = $file;
$this->cacheTime = $cacheTime;
}
/**
* 取得快取內容
* @param bool $output true=直接輸出並導向, false=回傳字串
* @return mixed false|string
*/
public function get($output = true) {
// 檢查:檔案存在、未過期、未強制重新整理
if (is_file($this->file)
&& (time() - filemtime($this->file)) <= $this->cacheTime
&& !isset($_GET['nocache'])) {
if ($output) {
// 直接導向快取檔案 (需為完整 HTML)
header('Location: ' . $this->file);
exit;
} else {
return file_get_contents($this->file);
}
}
return false;
}
/**
* 寫入快取內容
* @param string $content HTML 字串
*/
public function set($content) {
$fp = fopen($this->file, 'wb');
if ($fp) {
fwrite($fp, $content);
fclose($fp);
// 可選:設定檔案權限
@chmod($this->file, 0644);
}
}
}
<?php
require 'PageCache.php';
// 1. 依據文章 ID 產生唯一快取檔名
$articleId = (int)($_GET['id'] ?? 0);
if (!$articleId) { header('HTTP/1.1 404 Not Found'); exit; }
$cacheFile = __DIR__ . '/cache/article_' . $articleId . '.html';
$cache = new PageCache($cacheFile, 3600); // 1小時
// 2. 嘗試讀取快取
if ($cache->get()) {
// get() 內部已處理輸出並 exit,程式不會執行到這裡
}
// 3. 快取未命中:執行昂貴操作 (查詢 DB、渲染模板)
$article = $db->query("SELECT * FROM articles WHERE id = ?", [$articleId])->fetch();
if (!$article) { header('HTTP/1.1 404 Not Found'); exit; }
// 渲染 HTML (此處簡化,實際建議用模板引擎)
ob_start();
include 'article_template.php';
$html = ob_get_clean();
// 4. 寫入快取
$cache->set($html);
// 5. 輸出
echo $html;
<?php
// admin/update_article.php
$articleId = (int)$_POST['id'];
$db->update('articles', ['title'=>$_POST['title'], 'content'=>$_POST['content']], ['id'=>$articleId]);
// 刪除對應快取
$cacheFile = __DIR__ . '/../cache/article_' . $articleId . '.html';
if (file_exists($cacheFile)) unlink($cacheFile);
// 同步清除列表頁快取
$listCacheDir = __DIR__ . '/../cache/';
foreach (glob($listCacheDir . 'list_page_*.html') as $f) unlink($f);
echo "更新成功,快取已清除";
class TaggedPageCache extends PageCache {
private $tagFile;
public function __construct($file, $cacheTime = 3600, $tag = '') {
parent::__construct($file, $cacheTime);
$this->tagFile = dirname($file) . '/.tags_' . md5($tag) . '.txt';
}
public function set($content) {
parent::set($content);
// 記錄此快取屬於哪個標籤
file_put_contents($this->tagFile, $this->file . PHP_EOL, FILE_APPEND | LOCK_EX);
}
public static function clearByTag($cacheDir, $tag) {
$tagFile = $cacheDir . '/.tags_' . md5($tag) . '.txt';
if (file_exists($tagFile)) {
$files = file($tagFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($files as $f) { if (file_exists($f)) @unlink($f); }
@unlink($tagFile);
}
}
}
// 簡易鎖機制
$lockFile = $cacheFile . '.lock';
$fp = fopen($lockFile, 'w');
if (flock($fp, LOCK_EX | LOCK_NB)) {
// 取得鎖,重建快取
$html = renderPage();
$cache->set($html);
flock($fp, LOCK_UN);
} else {
// 未取得鎖,等待並重試讀取
sleep(1);
echo file_get_contents($cacheFile);
}
fclose($fp);
# /etc/fstab
tmpfs /var/www/html/cache tmpfs defaults,size=100M,uid=www-data,gid=www-data 0 0
將快取目錄掛載在記憶體,獲得接近 Redis 的讀寫速度,且無需額外服務。
md5() 或 sha1() 雜湊長參數:cache_' . md5($queryString) . '.htmlcache/a/b/cache_abc123.html 避免單目錄檔案過多| 方案 | 速度 | 持久性 | 跨伺服器 | 複雜度 | 適用場景 |
|---|---|---|---|---|---|
| PageCache (檔案) | 中 (SSD/tmpfs 快) | 永久 | 需共享儲存 | 極低 | 單機、虛擬主機、靜態頁面 |
| APCu | 極快 | 重啟消失 | 否 | 低 | 單機、熱點資料 |
| Redis | 極快 | 可持久化 | 是 | 中 | 叢集、分散式、需 TTL/結構化 |
| OPcache | 極快 | 記憶體 | 否 | 零設定 | PHP 位元碼快取 (必開) |
PageCache 類別以不到 50 行程式碼實現了實用的頁面級快取,核心邏輯是 filemtime() 判斷過期。適用於流量中等、內容更新頻率可預測的場景。生產環境建議搭配 tmpfs、鎖機制、標籤清除等進階功能,或直接升級至 Redis 等專業快取中介軟體。