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

ThinkPHP 檔案快取類原始碼解析與現代化重構建議

     編輯:PHP 綜合知識

ThinkPHP 內建的檔案快取驅動(`File`)採用單檔案儲存 JSON 序列化資料,結構簡單、零依賴,適合中小型專案。本文解析其核心原始碼,指出潛在問題(競態條件、序列化效能、目錄權限),並提供現代化重構版本(支援 PSR-16、標籤清除、原子寫入)。

原始碼核心邏輯(ThinkPHP 3.x/5.x 精簡版)

class Cache {
    const CACHE_DIR = '/Runtime/Cache/';
    const EXT = '.php'; // 實際為 .php 檔,內容為 

    private $dir = '';

    public function __construct(string $dir = '') { $this->dir = $dir; }

    public function set(string $key, mixed $data, int $expire = 0): bool {
        $filename = $this->getFilename($key);
        // 簡易去重:比對 MD5
        if (file_exists($filename)) {
            $old = $this->get($key);
            if (md5($old) === md5(json_encode($data))) return true;
        }
        $this->ensureDir(dirname($filename));
        $content = "";
        return file_put_contents($filename, $content, LOCK_EX) !== false;
    }

    public function get(string $key): mixed {
        $filename = $this->getFilename($key);
        if (!file_exists($filename)) return false;
        $data = include $filename; // 直接 include 回傳陣列
        // 這裡缺少過期時間檢查!原版依賴檔案 mtime 或獨立快取元資料
        return $data;
    }

    public function del(string $key): bool {
        $filename = $this->getFilename($key);
        return file_exists($filename) ? unlink($filename) : false;
    }

    private function getFilename(string $key): string {
        return __DIR__ . self::CACHE_DIR . $this->dir . md5($key) . self::EXT;
    }
    private function ensureDir(string $path): void {
        if (!is_dir($path)) mkdir($path, 0755, true);
    }
}

原版關鍵問題分析

問題影響說明
缺少過期時間檢查資料永不過期`get()` 未檢查 `$expire`,需額外機制(如獨立 meta 檔或檔名編碼時間)
`var_export` 序列化效能差、不支援閉包/資源大型陣列輸出極慢,建議改用 `igbinary` 或 `msgpack`
無原子寫入保證高併發下可能讀到半寫入檔案建議寫入暫存檔再 `rename()`
目錄權限 0777安全風險共享主機環境極危險,建議 0755 + 正確擁有者
不支援標籤/群組清除無法批次失效相關快取需額外索引機制

現代化重構版:支援 PSR-16、標籤、原子操作

<?php
declare(strict_types=1);

namespace App\Cache;

use Psr\SimpleCache\CacheInterface;
use RuntimeException;

class FileCache implements CacheInterface
{
    private string $directory;
    private int $defaultTtl;
    private string $extension = '.cache.php';
    private array $index = []; // key => [expire, tags]

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

    public function get(string $key, mixed $default = null): mixed
    {
        $this->validateKey($key);
        $meta = $this->index[$key] ?? null;
        if (!$meta) return $default;
        if ($meta['expire'] > 0 && $meta['expire'] < time()) {
            $this->delete($key);
            return $default;
        }
        $file = $this->filePath($key);
        if (!file_exists($file)) return $default;
        $data = @include $file;
        return $data === false ? $default : $data;
    }

    public function set(string $key, mixed $value, int $ttl = null): bool
    {
        $this->validateKey($key);
        $ttl = $ttl ?? $this->defaultTtl;
        $expire = $ttl > 0 ? time() + $ttl : 0;
        $file = $this->filePath($key);
        $tmpFile = $file . '.tmp.' . bin2hex(random_bytes(4));

        $content = "";
        if (file_put_contents($tmpFile, $content, LOCK_EX) === false) {
            @unlink($tmpFile);
            return false;
        }
        if (!@rename($tmpFile, $file)) {
            @unlink($tmpFile);
            return false;
        }
        @chmod($file, 0644);

        $this->index[$key] = ['expire' => $expire, 'tags' => []];
        $this->saveIndex();
        return true;
    }

    public function delete(string $key): bool
    {
        $this->validateKey($key);
        $file = $this->filePath($key);
        $deleted = file_exists($file) && unlink($file);
        if (isset($this->index[$key])) {
            unset($this->index[$key]);
            $this->saveIndex();
        }
        return $deleted;
    }

    public function clear(): bool
    {
        $files = glob($this->directory . '*' . $this->extension);
        foreach ($files as $f) @unlink($f);
        $this->index = [];
        $this->saveIndex();
        return true;
    }

    public function getMultiple(iterable $keys, mixed $default = null): iterable
    {
        $result = [];
        foreach ($keys as $key) $result[$key] = $this->get($key, $default);
        return $result;
    }

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

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

    public function has(string $key): bool
    {
        return $this->get($key) !== null;
    }

    // --- 擴充功能:標籤支援 ---
    public function setWithTags(string $key, mixed $value, array $tags, int $ttl = null): bool
    {
        if (!$this->set($key, $value, $ttl)) return false;
        $this->index[$key]['tags'] = $tags;
        foreach ($tags as $tag) {
            $tagFile = $this->directory . '.tags.' . md5($tag) . '.json';
            $tagKeys = file_exists($tagFile) ? json_decode(file_get_contents($tagFile), true) : [];
            $tagKeys[$key] = true;
            file_put_contents($tagFile, json_encode(array_keys($tagKeys)), LOCK_EX);
        }
        $this->saveIndex();
        return true;
    }

    public function deleteByTag(string $tag): int
    {
        $tagFile = $this->directory . '.tags.' . md5($tag) . '.json';
        if (!file_exists($tagFile)) return 0;
        $keys = json_decode(file_get_contents($tagFile), true);
        $count = 0;
        foreach ($keys as $key => _) {
            if ($this->delete($key)) $count++;
        }
        @unlink($tagFile);
        return $count;
    }

    // --- 內部方法 ---
    private function filePath(string $key): string
    {
        return $this->directory . md5($key) . $this->extension;
    }

    private function validateKey(string $key): void
    {
        if (strpos($key, '..') !== false || strpos($key, '/') !== false) {
            throw new RuntimeException('Invalid cache key');
        }
    }

    private function loadIndex(): void
    {
        $indexFile = $this->directory . '.index.json';
        if (file_exists($indexFile)) {
            $this->index = json_decode(file_get_contents($indexFile), true) ?? [];
        }
    }

    private function saveIndex(): void
    {
        $indexFile = $this->directory . '.index.json';
        file_put_contents($indexFile, json_encode($this->index), LOCK_EX);
    }
}

使用範例

$cache = new FileCache(__DIR__ . '/runtime/cache', 3600);

// 基本操作
$cache->set('user:1', ['name' => 'John', 'role' => 'admin'], 1800);
$user = $cache->get('user:1');

// 標籤批次清除
$cache->setWithTags('article:100', $content, ['article', 'category:tech'], 3600);
$cache->setWithTags('article:101', $content, ['article', 'category:tech'], 3600);
$cache->deleteByTag('category:tech'); // 一次清除該分類所有文章快取

// PSR-16 相容:可直接注入支援 PSR-16 的服務容器

效能優化建議

  • 序列化:安裝 `igbinary` 擴展,`ini_set('session.serialize_handler', 'igbinary')`,並將 `var_export` 改為 `igbinary_serialize()` / `unserialize()`。
  • 記憶體檔案系統:將快取目錄掛載至 `tmpfs`(`mount -t tmpfs -o size=512m tmpfs /var/www/cache`),I/O 速度提升 10-50 倍。
  • 避免大鍵值:單一快取項目建議 < 1MB,大物件拆分或改用 Redis。
  • 定期清理:排程執行 `find /cache -type f -mtime +7 -delete` 清除孤兒檔案。

檔案快取雖不如 Redis 功能豐富,但零依賴、部署簡單、適合中小流量或無法安裝擴展的環境。採用 PSR-16 介面並加強原子性、標籤支援後,足以應付大多數實務場景。

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