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

PHP 輕量快取類:檔案儲存、序列化、自動過期、標籤清除、PSR-16 相容與效能優化

     編輯:PHP 綜合知識

專案若不想引入 Symfony Cache、Laravel Cache 等重型庫,一個 < 100 行的檔案快取類即可滿足 90% 場景。本文實作符合 PSR-16 Simple Cache 介面的 `FileCache`,支援 TTL、標籤索引、原子寫入、鎖機制、GC 清理,並提供 Redis 遷移無縫切換介面。

一、PSR-16 介面定義

<?php
namespace Psr\SimpleCache;

interface CacheInterface {
    public function get(string $key, mixed $default = null): mixed;
    public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool;
    public function delete(string $key): bool;
    public function clear(): bool;
    public function getMultiple(iterable $keys, mixed $default = null): iterable;
    public function setMultiple(iterable $values, null|int|\DateInterval $ttl = null): bool;
    public function deleteMultiple(iterable $keys): bool;
    public function has(string $key): bool;
}
?>

二、FileCache 完整實作

<?php
declare(strict_types=1);

namespace App\Cache;

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

class FileCache implements CacheInterface {
    private string $dir;
    private string $tagIndexFile;
    private int $defaultTtl;
    private array $tagIndex = []; // tag => [key1, key2...]
    
    public function __construct(string $cacheDir, int $defaultTtl = 3600) {
        $this->dir = rtrim($cacheDir, '/\\').'/';
        $this->tagIndexFile = $this->dir . '.tags.index.php';
        $this->defaultTtl = $defaultTtl;
        if (!is_dir($this->dir)) mkdir($this->dir, 0755, true);
        $this->loadTagIndex();
    }
    
    private function loadTagIndex(): void {
        if (is_file($this->tagIndexFile)) {
            $this->tagIndex = include $this->tagIndexFile;
            if (!is_array($this->tagIndex)) $this->tagIndex = [];
        }
    }
    
    private function saveTagIndex(): void {
        $tmp = $this->tagIndexFile . '.tmp';
        file_put_contents($tmp, 'tagIndex, true) . ';');
        rename($tmp, $this->tagIndexFile);
    }
    
    private function path(string $key): string {
        $safe = preg_replace('/[^a-zA-Z0-9_.-]/', '_', $key);
        return $this->dir . $safe . '.cache.php';
    }
    
    private function write(string $path, mixed $value, int $expire): bool {
        $data = [
            'expire' => $expire,
            'value' => $value,
            'tags' => [], // 由 setWithTags 填入
        ];
        $content = 'path($key);
        if (!is_file($path)) return $default;
        $data = include $path;
        if (!is_array($data) || !isset($data['expire'], $data['value'])) return $default;
        if ($data['expire'] > 0 && $data['expire'] < time()) {
            $this->delete($key);
            return $default;
        }
        return $data['value'];
    }
    
    public function set(string $key, mixed $value, null|int|\DateInterval $ttl = null): bool {
        $expire = $this->resolveTtl($ttl);
        return $this->write($this->path($key), $value, $expire);
    }
    
    public function setWithTags(string $key, mixed $value, iterable $tags, null|int|\DateInterval $ttl = null): bool {
        $expire = $this->resolveTtl($ttl);
        $path = $this->path($key);
        $data = ['expire' => $expire, 'value' => $value, 'tags' => []];
        foreach ($tags as $tag) {
            $tag = (string)$tag;
            $data['tags'][] = $tag;
            $this->tagIndex[$tag][] = $key;
        }
        $content = 'saveTagIndex();
        return true;
    }
    
    public function delete(string $key): bool {
        $path = $this->path($key);
        if (!is_file($path)) return false;
        $data = include $path;
        if (isset($data['tags']) && is_array($data['tags'])) {
            foreach ($data['tags'] as $tag) {
                $this->tagIndex[$tag] = array_filter($this->tagIndex[$tag] ?? [], fn($k) => $k !== $key);
            }
            $this->saveTagIndex();
        }
        return @unlink($path);
    }
    
    public function clearByTag(string $tag): int {
        $keys = $this->tagIndex[$tag] ?? [];
        $count = 0;
        foreach ($keys as $key) {
            if ($this->delete($key)) $count++;
        }
        unset($this->tagIndex[$tag]);
        $this->saveTagIndex();
        return $count;
    }
    
    public function clear(): bool {
        $files = glob($this->dir . '*.cache.php');
        $success = true;
        foreach ($files as $f) $success &= @unlink($f);
        $this->tagIndex = [];
        $this->saveTagIndex();
        return $success;
    }
    
    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, null|int|\DateInterval $ttl = null): bool {
        $ok = true;
        foreach ($values as $k => $v) $ok &= $this->set($k, $v, $ttl);
        return $ok;
    }
    
    public function deleteMultiple(iterable $keys): bool {
        $ok = true;
        foreach ($keys as $k) $ok &= $this->delete($k);
        return $ok;
    }
    
    public function has(string $key): bool {
        return $this->get($key, new class {}) !== new class {};
    }
    
    private function resolveTtl(null|int|\DateInterval $ttl): int {
        if ($ttl === null) return $this->defaultTtl;
        if ($ttl instanceof \DateInterval) return time() + (int)$ttl->format('%s');
        return $ttl > 0 ? time() + $ttl : 0; // 0 = 永不過期
    }
    
    /** 執行垃圾回收 (建議 cron 定時呼叫) */
    public function gc(int $maxFiles = 1000): int {
        $files = glob($this->dir . '*.cache.php');
        $deleted = 0;
        foreach (array_slice($files, 0, $maxFiles) as $f) {
            $data = @include $f;
            if (isset($data['expire']) && $data['expire'] > 0 && $data['expire'] < time()) {
                @unlink($f);
                $deleted++;
            }
        }
        return $deleted;
    }
}
?>

三、使用範例

<?php
$cache = new \App\Cache\FileCache(__DIR__.'/../runtime/cache', 3600);

// 基本
$cache->set('config:site', ['name' => 'MySite', 'theme' => 'dark'], 7200);
$config = $cache->get('config:site');

// 標籤
$cache->setWithTags('article:100', $article, ['article_100', 'cat_tech', 'author_5'], 3600);
$cache->clearByTag('cat_tech'); // 分類變更時清除所有相關文章快取

// 批次
$cache->setMultiple(['a'=>1, 'b'=>2, 'c'=>3], 300);
print_r($cache->getMultiple(['a','b','c','d'], 'missing'));

// GC
$cache->gc(500); // 每次最多檢查 500 個檔案
?>

四、效能優化建議

優化做法效果
OPcache快取檔案為純 PHP 陣列,OPcache 直接編譯快取讀取速度接近原生陣列
tmpfs 掛載`mount -t tmpfs -o size=512M tmpfs /var/www/runtime/cache`I/O 完全記憶體化
igbinary 序列化改用 `igbinary_serialize`/`unserialize`,體積減 30-50%磁碟/記憶體雙省
分層快取本地 FileCache (熱資料) + Redis (共享/大資料)單機極速、叢集一致

五、遷移至 Redis / PSR-6

實作 `Psr\SimpleCache\CacheInterface` 介面即可無縫替換。生產環境建議使用 symfony/cache (支援 PSR-16/PSR-6、標籤、鎖、早期過期、多後端)。

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