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

PHP 檔案快取類實作:從基礎頁面快取到 PSR-16 相容適配器

     編輯:PHP 相關知識

輕量級檔案快取系統設計:支援標籤、原子操作、自動清理、PSR-16 介面

檔案快取是中小型專案、無 Redis 環境、靜態內容生成的首選。本文從簡單的頁面快取類演進至生產級 FileCacheAdapter,實現 PSR-16 CacheInterface,支援 TTL、標籤失效、原子寫入、並發鎖、自動 GC。

階段一:簡單頁面快取(輸出緩衝)

<?php
class SimplePageCache {
    private string $cacheDir;
    private int $ttl;

    public function __construct(string $cacheDir, int $ttl = 3600) {
        $this->cacheDir = rtrim($cacheDir, '/');
        $this->ttl = $ttl;
        if (!is_dir($this->cacheDir)) mkdir($this->cacheDir, 0755, true);
    }

    public function start(string $key): bool {
        $file = $this->getFilePath($key);
        if (file_exists($file) && (time() - filemtime($file) < $this->ttl)) {
            readfile($file);
            return true; // 快取命中,直接輸出
        }
        ob_start(); // 開啟輸出緩衝
        return false; // 快取未命中,繼續執行業務邏輯
    }

    public function end(string $key): void {
        $content = ob_get_clean();
        $this->save($key, $content);
        echo $content;
    }

    private function getFilePath(string $key): string {
        return $this->cacheDir . '/' . md5($key) . '.html';
    }

    private function save(string $key, string $content): void {
        $file = $this->getFilePath($key);
        $tmp = $file . '.tmp.' . bin2hex(random_bytes(4));
        file_put_contents($tmp, $content);
        rename($tmp, $file); // 原子替換
    }
}

// 使用
$cache = new SimplePageCache(__DIR__.'/cache/pages', 300);
if (!$cache->start('homepage')) {
    // 業務邏輯:查詢 DB、渲染模板...
    echo '

首頁內容

'; $cache->end('homepage'); } ?>

階段二:通用鍵值快取(支援陣列/物件序列化)

<?php
class FileCache {
    private string $dir;
    private int $defaultTtl;
    private string $ext = '.cache.php'; // 使用 var_export 格式,opcache 友善

    public function __construct(string $dir, int $defaultTtl = 3600) {
        $this->dir = rtrim($dir, '/');
        $this->defaultTtl = $defaultTtl;
        if (!is_dir($this->dir)) mkdir($this->dir, 0755, true);
    }

    public function get(string $key, mixed $default = null): mixed {
        $file = $this->path($key);
        if (!file_exists($file)) return $default;
        if ($this->isExpired($file)) {
            $this->delete($key);
            return $default;
        }
        return require $file; // 直接 require,opcache 加速
    }

    public function set(string $key, mixed $value, int $ttl = null): bool {
        $ttl = $ttl ?? $this->defaultTtl;
        $expire = time() + $ttl;
        $content = "path($key);
        $tmp = $file . '.tmp.' . bin2hex(random_bytes(4));
        if (file_put_contents($tmp, $content) === false) return false;
        if (!@rename($tmp, $file)) { @unlink($tmp); return false; }
        return true;
    }

    public function delete(string $key): bool {
        $file = $this->path($key);
        return file_exists($file) && @unlink($file);
    }

    public function clear(): bool {
        $files = glob($this->dir . '/*' . $this->ext);
        foreach ($files as $f) @unlink($f);
        return true;
    }

    public function has(string $key): bool {
        $file = $this->path($key);
        return file_exists($file) && !$this->isExpired($file);
    }

    private function path(string $key): string {
        $safe = preg_replace('/[^a-zA-Z0-9_-]/', '', $key);
        return $this->dir . '/' . $safe . $this->ext;
    }

    private function isExpired(string $file): bool {
        $line = @file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES)[0] ?? '';
        return preg_match('/EXPIRE:\s*(\d+)/', $line, $m) && (int)$m[1] < time();
    }
}
?>

階段三:PSR-16 相容適配器(可直接注入 Laravel/Symfony/任何框架)

composer require psr/simple-cache ^1.0 | ^2.0 | ^3.0

<?php
namespace App\Cache;

use Psr\SimpleCache\CacheInterface;
use Psr\SimpleCache\InvalidArgumentException;

class FileCacheAdapter implements CacheInterface {
    private FileCache $cache; // 使用上述 FileCache

    public function __construct(string $directory, int $defaultTtl = 3600) {
        $this->cache = new FileCache($directory, $defaultTtl);
    }

    public function get(string $key, mixed $default = null): mixed {
        $this->validateKey($key);
        return $this->cache->get($key, $default);
    }

    public function set(string $key, mixed $value, int $ttl = null): bool {
        $this->validateKey($key);
        return $this->cache->set($key, $value, $ttl);
    }

    public function delete(string $key): bool {
        $this->validateKey($key);
        return $this->cache->delete($key);
    }

    public function clear(): bool {
        return $this->cache->clear();
    }

    public function has(string $key): bool {
        $this->validateKey($key);
        return $this->cache->has($key);
    }

    // PSR-16 批次操作(選擇性實現,預設拋出異常)
    public function getMultiple(iterable $keys, mixed $default = null): iterable {
        $result = [];
        foreach ($keys as $key) {
            $this->validateKey($key);
            $result[$key] = $this->get($key, $default);
        }
        return $result;
    }

    public function setMultiple(iterable $values, int $ttl = null): bool {
        foreach ($values as $key => $value) {
            $this->validateKey($key);
            if (!$this->set($key, $value, $ttl)) return false;
        }
        return true;
    }

    public function deleteMultiple(iterable $keys): bool {
        foreach ($keys as $key) {
            $this->validateKey($key);
            if (!$this->delete($key)) return false;
        }
        return true;
    }

    private function validateKey(string $key): void {
        if (!preg_match('/^[a-zA-Z0-9_\-\.]+$/', $key)) {
            throw new InvalidArgumentException("Invalid cache key: $key");
        }
    }
}
?>

進階功能:標籤失效

為快取項打標籤,實現「清除所有 `user:123` 相關快取」:

<?php
class TaggedFileCache extends FileCache {
    private string $tagDir;

    public function __construct(string $dir, int $defaultTtl = 3600) {
        parent::__construct($dir, $defaultTtl);
        $this->tagDir = $this->dir . '/_tags';
        if (!is_dir($this->tagDir)) mkdir($this->tagDir, 0755, true);
    }

    public function set(string $key, mixed $value, int $ttl = null, array $tags = []): bool {
        if (!parent::set($key, $value, $ttl)) return false;
        foreach ($tags as $tag) {
            $tagFile = $this->tagDir . '/' . md5($tag) . '.txt';
            // 記錄 key 歸屬標籤(追加模式,鎖保護)
            $fp = fopen($tagFile, 'c');
            flock($fp, LOCK_EX);
            fwrite($fp, $key . "\n");
            flock($fp, LOCK_UN);
            fclose($fp);
        }
        return true;
    }

    public function flushByTag(string $tag): int {
        $tagFile = $this->tagDir . '/' . md5($tag) . '.txt';
        if (!file_exists($tagFile)) return 0;
        $keys = array_filter(array_map('trim', file($tagFile)));
        $count = 0;
        foreach ($keys as $key) {
            if ($this->delete($key)) $count++;
        }
        @unlink($tagFile); // 清除標籤索引
        return $count;
    }
}
?>

自動垃圾回收(GC)

<?php
// 併入 FileCache
public function gc(int $probability = 10): int {
    // 機率性觸發:1/$probability 請求執行 GC
    if (mt_rand(1, $probability) !== 1) return 0;
    
    $files = glob($this->dir . '/*' . $this->ext);
    $deleted = 0;
    foreach ($files as $file) {
        if ($this->isExpired($file)) {
            @unlink($file);
            $deleted++;
        }
    }
    return $deleted;
}

// 在 get()/set() 開頭調用 $this->gc();
?>

效能優化清單

優化點實現
OpCache 加速使用 .cache.php + var_export + require,避免 unserialize 開銷
原子寫入臨時檔 rename() 替換,防止讀取到半寫入內容
並發鎖標籤索引寫入使用 flock();高併發讀取無需鎖(檔案系統原子性)
目錄分片大量快取時,按 md5($key)[0..1] 建立子目錄,避免單目錄檔案過多
記憶體映射極高頻讀取可考慮 mmap 或直接載入 PHP 陣列(需評估記憶體)

何時不應使用檔案快取

  • 分散式部署(多台 Web Server) → 必須用 Redis/Memcached
  • 高併發寫入(>100 QPS 寫) → 檔案鎖爭用嚴重
  • 大體積值(>1MB) → 檔案 I/O 瓶頸,考慮物件儲存
  • 需要原子增減/集合操作 → Redis 原生支援

總結

FileCacheAdapter 提供零依賴、PSR-16 相容、生產級的檔案快取方案。配合 OpCache、原子寫入、標籤失效、機率 GC,足以支撐中小型專案的全站快取需求。遷移至 Redis 時,只需替換適配器實現,業務代碼零修改。

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