首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

PHP 檔案快取類別實作:自動快取、過期清理與效能優化

     編輯:PHP 綜合知識

頁面級檔案快取是 PHP 提升回應速度、降低資料庫負載的經典技術。本文實作一個生產就緒的快取類別,支援 GET 請求自動快取、過期時間控制、目錄分散儲存、過期檔案自動清理、POST 請求自動失效等功能。

完整快取類別

<?php
declare(strict_types=1);

class FileCache {
    private string $cacheRoot;
    private int $ttl;
    private string $extension;
    private string $cacheFile;
    private bool $enabled;

    public function __construct(
        string $cacheRoot = '/var/www/cache',
        int $ttl = 1800,
        string $extension = '.html'
    ) {
        $this->cacheRoot  = rtrim($cacheRoot, '/');
        $this->ttl        = $ttl;
        $this->extension  = $extension;
        $this->enabled    = $_SERVER['REQUEST_METHOD'] === 'GET';
        
        if ($this->enabled) {
            $this->initCachePath();
        }
    }

    private function initCachePath(): void {
        $uri = $_SERVER['REQUEST_URI'] ?? '/';
        $key = md5($uri);
        $subDir = $this->cacheRoot . '/' . $key[0]; // 一級分散
        $this->cacheFile = $subDir . '/' . $key . $this->extension;
        
        // 建立目錄
        if (!is_dir($this->cacheRoot)) {
            mkdir($this->cacheRoot, 0755, true);
        }
        if (!is_dir($subDir)) {
            mkdir($subDir, 0755, true);
        }
    }

    /**
     * 嘗試讀取快取,命中則直接輸出並結束腳本
     */
    public function tryServe(): void {
        if (!$this->enabled) return;
        
        if (is_file($this->cacheFile) && (time() - filemtime($this->cacheFile) < $this->ttl)) {
            // 快取命中
            readfile($this->cacheFile);
            exit;
        }
        
        // 未命中或過期:啟用輸出緩衝,腳本結束時自動寫入
        ob_start([$this, 'writeCache']);
    }

    /**
     * 輸出緩衝回呼:將內容寫入快取檔案
     */
    public function writeCache(string $content): string {
        if (!$this->enabled) return $content;
        
        $tmpFile = $this->cacheFile . '.tmp';
        $bytes = @file_put_contents($tmpFile, $content, LOCK_EX);
        if ($bytes !== false) {
            @rename($tmpFile, $this->cacheFile); // 原子替換
            @chmod($this->cacheFile, 0644);
            $this->cleanExpired(); // 隨機觸發清理
        }
        return $content;
    }

    /**
     * 清理過期快取檔案
     */
    private function cleanExpired(): void {
        // 簡單機率觸發,避免每次請求掃描
        if (mt_rand(1, 100) > 5) return; // 5% 機率
        
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($this->cacheRoot, FilesystemIterator::SKIP_DOTS)
        );
        $now = time();
        foreach ($iterator as $file) {
            if ($file->isFile() && $now - $file->getMTime() > $this->ttl) {
                @unlink($file->getPathname());
            }
        }
        // 清理空目錄
        $this->removeEmptyDirs($this->cacheRoot);
    }

    private function removeEmptyDirs(string $dir): void {
        $items = scandir($dir);
        if (count($items) <= 2) { // 僅 . ..
            @rmdir($dir);
            return;
        }
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') continue;
            $path = $dir . '/' . $item;
            if (is_dir($path)) {
                $this->removeEmptyDirs($path);
            }
        }
    }

    /**
     * 手動清除特定 URI 快取
     */
    public function purge(string $uri): bool {
        $key = md5($uri);
        $file = $this->cacheRoot . '/' . $key[0] . '/' . $key . $this->extension;
        return is_file($file) && @unlink($file);
    }

    /**
     * 手動清除所有快取
     */
    public function purgeAll(): void {
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($this->cacheRoot, FilesystemIterator::SKIP_DOTS),
            RecursiveIteratorIterator::CHILD_FIRST
        );
        foreach ($iterator as $file) {
            if ($file->isFile()) @unlink($file->getPathname());
            else @rmdir($file->getPathname());
        }
    }
}

使用方式

1. 入口檔案最上方引入(在任何輸出前)

<?php
require_once __DIR__ . '/FileCache.php';

$cache = new FileCache(
    cacheRoot: __DIR__ . '/../cache', // 建議放在非公開目錄
    ttl: 1800,                        // 30 分鐘
    extension: '.cache.html'
);
$cache->tryServe(); // 命中則直接 exit

// ... 後續正常業務邏輯、資料庫查詢、模板渲染 ...
?>

2. 配合框架/模板引擎

<?php
// Laravel 風格中介層
public function handle($request, Closure $next) {
    if ($request->isMethod('GET')) {
        $cache = new FileCache();
        $cache->tryServe(); // 命中直接返回 Response
    }
    return $next($request);
}

關鍵設計細節解析

特性實作細節效益
一級子目錄分散md5($uri)[0] 建立 16 個子目錄避免單目錄檔案過多導致 inode 耗盡、ls 緩慢
原子寫入寫入 .tmp → rename() 覆蓋避免並發讀取到半寫入檔案
POST 自動失效建構子檢查 REQUEST_METHOD表單提交後立即看到最新資料
機率性清理mt_rand(1,100) > 95 觸發分攤清理成本,避免請求阻塞
輸出緩衝回呼ob_start([$this, 'writeCache'])零侵入式攔截輸出,不修改業務邏輯
非公開目錄cacheRoot 置於 public/ 之外防止快取檔案被直接下載洩露資料

進階擴充建議

  1. 標籤式失效:快取檔名含標籤 tag:user:123,更新用戶時掃描標籤清除。
  2. 壓縮儲存:gzencode($content, 6) 寫入,readfile 配合 Content-Encoding: gzip 輸出。
  3. 快取預熱:部署後爬蟲預先請求熱門頁面建立快取。
  4. 監控指標:記錄命中率、平均讀取時間、快取大小,接入 Prometheus/Grafana。
  5. 分散式快取:高流量遷移至 Redis/Memcached,介面保持相容。

常見問題對照

現象原因解決
快取不生效,每次都重新渲染輸出前已有 echo/空白導致 ob_start 失效確保 tryServe() 在任何輸出前呼叫
登入狀態被快取,不同用戶看到同內容URI 相同但內容因 Session 而異快取鍵加入 Session ID:md5($uri . session_id()) 或改用片段快取
權限錯誤無法建立目錄/寫入PHP-FPM 執行使用者無權限chown www-data:www-data cache/、chmod 755
磁碟空間耗盡清理機制失效、TTL 過長降低 TTL、加入 cron 定時清理、監控磁碟使用率

結語:檔案快取類別以 ob_start 回呼實現零侵入式頁面快取,配合目錄分散、原子寫入、機率清理、POST 失效等生產級特性,可大幅降低資料庫壓力、提升首屏渲染速度。隨著流量增長,建議平滑遷移至 Redis 等分散式快取,介面設計保持相容即可無縫切換。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved