在高並發 Web 應用中,數據庫查詢往往是性能瓶頸。本文介紹一個通過繼承 PDO 類實現的透明查詢緩存方案,支持自動緩存、過期機制、序列化存儲,可無縫替換原有 query() 調用,大幅降低數據庫負載。
class myPDO extends PDO {
public $cache_Dir = null; // 緩存目錄
public $cache_expireTime = 7200; // 緩存過期時間(秒),默認2小時
// 帶緩存的查詢方法
public function cquery($sql) {
// 1. 校驗緩存目錄
if ($this->cache_Dir === null || !is_dir($this->cache_Dir)) {
exit("緩存目錄配置錯誤!");
}
$this->cache_Dir = str_replace("\\", "/", $this->cache_Dir);
$FileName = rtrim($this->cache_Dir, "/") . "/" . urlencode(trim($sql)) . ".sql";
// 2. 判斷是否需要重新查詢
if (!file_exists($FileName) || time() - filemtime($FileName) > $this->cache_expireTime) {
if ($tmpRS = parent::query($sql)) {
$data = serialize($tmpRS->fetchAll());
self::createFile($FileName, $data);
} else {
exit("SQL語法錯誤
");
}
}
// 3. 返回緩存結果
return $this->readCache($FileName);
}
// 讀取緩存文件
private static function readCache($FilePath) {
if (is_file($FilePath) && $Data = file_get_contents($FilePath)) {
return new cache_PDOStatement(unserialize($Data));
}
return false;
}
// 創建緩存文件
public static function createFile($FilePath, $Data = '') {
return file_put_contents($FilePath, $Data) !== false;
}
}
class cache_PDOStatement {
private $recordArr = [];
private $cursorId = 0;
private $recordCount = 0;
public function __construct($arr) {
$this->recordArr = $arr;
$this->recordCount = count($arr);
}
// 模擬 PDOStatement::fetch()
public function fetch() {
if ($this->cursorId >= $this->recordCount) return false;
if ($this->cursorId === 0) {
$this->cursorId++;
return current($this->recordArr);
}
$this->cursorId++;
return next($this->recordArr);
}
// 模擬 PDOStatement::fetchAll()
public function fetchAll() {
return $this->recordArr;
}
// 模擬 PDOStatement::fetchColumn()
public function fetchColumn() {
$tmpArr = current($this->recordArr);
return $tmpArr[0] ?? null;
}
}
<?php
// 1. 初始化連接
$db = new myPDO('mysql:host=localhost;dbname=news', 'newsadmin', '123456');
// 2. 配置緩存
$db->cache_Dir = "cache"; // 緩存目錄需可寫
$db->cache_expireTime = 7200; // 2小時過期
// 3. 使用緩存查詢(替換原 query())
$rs = $db->cquery("SELECT * FROM news LIMIT 0,10");
while ($row = $rs->fetch()) {
echo $row["F_title"] . "
";
}
// 4. 清理資源
$rs = null;
$db = null;
?>
PDOStatement 接口,業務代碼無需修改urlencode($sql) 作為文件名,自動區分不同查詢| 問題 | 風險等級 | 改進建議 |
|---|---|---|
| SQL 注入風險 | 高 | 緩存鍵使用原始 SQL,若含用戶輸入可能導致緩存污染或路徑遍歷 |
| 緩存鍵過長 | 中 | 文件系統對文件名長度有限制,建議使用 hash($sql) 作為文件名 |
| 無緩存清理機制 | 中 | 需定期清理過期文件,避免磁盤佔用過大 |
| 並發寫入競爭 | 低 | 高並發下可能重複查詢,可加文件鎖或雙重檢查 |
| 不支持參數綁定 | 高 | 無法緩存預處理語句,建議擴展支持 prepare()/execute() |
// 使用 SHA256 哈希避免文件名過長及特殊字符問題
private function getCacheKey($sql, $params = []) {
$keyData = $sql . serialize($params);
return hash('sha256', $keyData) . '.cache';
}
// 支持預處理語句緩存
public function cexecute($stmt, $params = []) {
$cacheKey = $this->getCacheKey($stmt->queryString, $params);
// ... 其餘邏輯類似
}
這個方案雖然簡單,但展示了「裝飾器模式」在數據庫訪問層的實際應用,是理解 ORM 緩存機制、查詢優化的良好切入點。