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

PHP 檔案快取機制完整實現:cache_write、cache_read、var_export 高效能資料序列化

     編輯:PHP 相關知識

為什麼需要檔案快取?

  • 減少資料庫重複查詢,降低 DB 負載
  • 避免重複計算 (如複雜報表、模板渲染)
  • 單機部署無需額外 Redis/Memcached 基建
  • 資料以 PHP 代碼形式儲存,include 直接回傳陣列/常數,效能極高

核心原理:var_export + include

<?php
$arr = ['name' => 'John', 'tags' => ['php', 'cache']];

// 1. var_export 產生合法 PHP 代碼字串
$code = '';
// 產生:  'John', 'tags' => array (0 => 'php', 1 => 'cache'),); ?>

// 2. 寫入檔案
file_put_contents('cache/user_123.php', $code);

// 3. 讀取時直接 include,回傳陣列
$data = include 'cache/user_123.php';
// $data 即為原始陣列,無需 unserialize/json_decode

優勢:OPcache 會編譯快取檔案,後續讀取極快;比 JSON/unserialize 少一次解析步驟。

完整快取工具類

<?php
class FileCache {
  private string $dir;
  private int $defaultTtl;
  
  public function __construct(string $dir = __DIR__ . '/cache', int $defaultTtl = 3600) {
    $this->dir = rtrim($dir, '/') . '/';
    $this->defaultTtl = $defaultTtl;
    if (!is_dir($this->dir)) mkdir($this->dir, 0755, true);
  }
  
  // 寫入快取 (支援陣列、常數陣列)
  public function write(string $key, mixed $data, int $ttl = 0): bool {
    $ttl = $ttl ?: $this->defaultTtl;
    $file = $this->dir . $this->sanitizeKey($key) . '.php';
    
    $content = $this->serializeData($data, $ttl);
    $bytes = @file_put_contents($file, $content, LOCK_EX);
    
    if ($bytes === false) return false;
    @chmod($file, 0644);
    return true;
  }
  
  // 讀取快取 (自動檢查過期)
  public function read(string $key, mixed $default = null): mixed {
    $file = $this->dir . $this->sanitizeKey($key) . '.php';
    
    if (!file_exists($file)) return $default;
    
    // 檢查過期 (檔案修改時間 + TTL)
    $metaFile = $file . '.meta';
    if (file_exists($metaFile)) {
      $meta = json_decode(file_get_contents($metaFile), true);
      if ($meta && (time() > $meta['expire'])) {
        $this->delete($key);
        return $default;
      }
    }
    
    // 直接 include 回傳資料
    return @include $file ?: $default;
  }
  
  // 刪除快取
  public function delete(string $key): bool {
    $file = $this->dir . $this->sanitizeKey($key) . '.php';
    $metaFile = $file . '.meta';
    $ok1 = @unlink($file);
    $ok2 = @unlink($metaFile);
    return $ok1 || $ok2;
  }
  
  // 清空所有快取
  public function clear(): int {
    $count = 0;
    foreach (glob($this->dir . '*.php') as $file) {
      if (@unlink($file)) $count++;
      @unlink($file . '.meta');
    }
    return $count;
  }
  
  // 取得快取資訊
  public function info(string $key): ?array {
    $file = $this->dir . $this->sanitizeKey($key) . '.php';
    if (!file_exists($file)) return null;
    
    $metaFile = $file . '.meta';
    $meta = file_exists($metaFile) ? json_decode(file_get_contents($metaFile), true) : [];
    
    return [
      'key' => $key,
      'size' => filesize($file),
      'mtime' => filemtime($file),
      'expire' => $meta['expire'] ?? null,
      'ttl' => $meta['ttl'] ?? null,
    ];
  }
  
  private function sanitizeKey(string $key): string {
    return preg_replace('/[^a-zA-Z0-9_-]/', '_', $key);
  }
  
  private function serializeData(mixed $data, int $ttl): string {
    $expire = time() + $ttl;
    
    // 寫入中繼資料 (過期時間)
    $metaFile = $this->dir . $this->sanitizeKey($key) . '.php.meta';
    file_put_contents($metaFile, json_encode(['expire' => $expire, 'ttl' => $ttl]));
    
    // 區分陣列類型輸出
    if (is_array($data)) {
      $keys = array_keys($data);
      $isAssoc = $keys !== range(0, count($keys) - 1);
      
      if ($isAssoc) {
        // 關聯陣列:輸出 define() 常數陣列 (可選)
        $code = " $v) {
          $code .= "define('" . strtoupper(preg_replace('/[^A-Z0-9_]/', '', $k)) . "', " . var_export($v, true) . ");\n";
        }
        $code .= "?>";
      } else {
        // 索引陣列:直接 return 陣列
        $code = "";
      }
    } else {
      // 純量值
      $code = "";
    }
    
    return $code;
  }
}

使用範例

<?php
require 'FileCache.php';

$cache = new FileCache(__DIR__ . '/cache', 1800); // 30分鐘預設 TTL

// 寫入
$userData = [
  'id' => 123,
  'name' => 'John Doe',
  'roles' => ['admin', 'editor'],
  'prefs' => ['theme' => 'dark', 'lang' => 'zh-TW']
];
$cache->write('user_123', $userData, 3600);

// 讀取
$user = $cache->read('user_123', []); // 不存在回傳空陣列
if ($user) {
  echo "用戶: {$user['name']}, 角色: " . implode(', ', $user['roles']);
}

// 條件式快取模式
function getExpensiveData($id, FileCache $cache) {
  $key = "expensive_data_$id";
  $data = $cache->read($key);
  
  if ($data === null) { // 快取未命中
    // 模擬昂貴操作
    sleep(2);
    $data = ['result' => 'computed_' . $id, 'time' => time()];
    $cache->write($key, $data, 600); // 快取 10 分鐘
  }
  return $data;
}

效能分析:何時快取有效?

a = 本機讀寫時間 (include + OPcache)
b = 本機佔用空間
c = 網路傳輸時間 (到 DB)
d = 資料庫服務器磁盤/CPU 時間

快取有效當:a + b < c + d

典型數值 (單次查詢):
- a: 0.1-0.5 ms (OPcache 後)
- c: 0.5-2 ms (本機網路)
- d: 1-10 ms (查詢執行)

結論:單機部署、查詢複雜、頻率高時效果明顯。
分散式部署時 c 變大,快取更有價值;但檔案快取需共享儲存 (NFS/GlusterFS) 或改用 Redis。

進階主題

1. 標籤式快取失效 (Tag-based Invalidation)

<?php
// 寫入時關聯標籤
$cache->write('article_1', $data, 3600, ['tag' => 'article', 'tag' => 'user_123']);

// 刪除所有 user_123 相關快取
$cache->deleteByTag('user_123'); // 需額外索引機制,檔案快取較難實現,建議用 Redis

2. 防止快取擊穿

<?php
function getWithLock(string $key, callable $fallback, FileCache $cache, int $ttl = 3600) {
  $data = $cache->read($key);
  if ($data !== null) return $data;
  
  // 簡易鎖 (檔案鎖)
  $lockFile = $cache->getDir() . ".lock_{$key}";
  $fp = fopen($lockFile, 'w');
  if (flock($fp, LOCK_EX | LOCK_NB)) {
    try {
      $data = $fallback(); // 只有持鎖者執行昂貴操作
      $cache->write($key, $data, $ttl);
    } finally {
      flock($fp, LOCK_UN);
      fclose($fp);
      @unlink($lockFile);
    }
  } else {
    // 等待持鎖者完成
    usleep(100000); // 100ms
    return $cache->read($key, $fallback());
  }
  return $data;
}

3. 熱點資料預熱

<?php
// 部署後/清空快取後,背景腳本預熱
foreach (getHotKeys() as $key) {
  $data = computeExpensive($key);
  $cache->write($key, $data, 3600);
}

避坑指南

坑後果解決
快取檔案無副檔名/.phpWeb 可直接訪問洩露資料目錄設 deny from all、或放在 Webroot 外
var_export 循環引用Fatal error / 無限遞迴資料結構扁平化、移除循環引用
大量小檔案 inode 耗盡無法建立新檔案合併快取、定期清理、用 Redis
併發寫入損壞檔案include 解析錯誤LOCK_EX + 原子寫入 (tmp rename)
TTL 未檢查直接讀取髒讀過期資料read() 內建檢查過期

何時升級 Redis?

  • 多機負載均衡 (需共享快取)
  • QPS > 5000 (檔案系統鎖競爭)
  • 需要原子操作 (INCR、Set、Sorted Set)
  • 需要發布/訂閱失效通知
  • 資料結構複雜 (Hash、List、Stream)
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved