红穆笔记
首頁 程序筆記 如何讓WordPress飛起來?我用SQLite懟掉了Redis
程序筆記 踩坑總結 wordpress

如何讓WordPress飛起來?我用SQLite懟掉了Redis

如何让WordPress飞起来?我用SQLite怼掉了Redis

我網站跑的是WordPress + 子比主題。子比這玩意兒,懂的都懂——登錄、會員、下載權限、QQ登錄、回覆可見……功能塞得滿滿當當,每次刷新都要查幾十次數據庫。

於是我想簡單改造一下。

代碼已經放好。
https://github.com/1810216796/wordpress-sqlite-cache/

這套方案到底幹了啥?

核心思路就兩條:

  1. 對象緩存(Object Cache):替代Redis,把數據庫查詢結果(如文章數據、用戶信息)存到SQLite文件裏。WordPress每次查詢前先問SQLite,有就直接拿,省掉MySQL的IO。

  2. 全頁面靜態緩存(Page Cache):對於不登錄的遊客,直接輸出整個頁面的HTML,連PHP模板渲染都省了,相當於把動態站僞裝成純靜態站。

這兩層一上,99元的服務器也能跑出“土豪服務器”的感覺。

第一步:ApiCache.php,替換Redis的活兒

這篇文章:高效率網頁緩存頁面代碼

這個類負責讀寫SQLite,支持MD5分片(把緩存分散到多個.db文件裏,避免單文件過大),還帶訪問計數,方便後續清理冷數據。

直接上代碼,保存爲/wp-content/ApiCache.php

<?php
// ApiCache.php - 完全版,包含 delete 方法

define('CACHE_ROOT', __DIR__ . '/Mcache');

if (!defined('CACHE_PROBABILITY')) {
    define('CACHE_PROBABILITY', 100);
}
if (!defined('CACHE_DEFAULT_SHARD')) {
    define('CACHE_DEFAULT_SHARD', 1); // 1=16×16, 2=16×256, 3=256×256
}

class ApiCache
{
    private $dirName;
    private $root;
    private $level1;
    private $level2;
    private $dbPool = [];
    private $obStack = [];

    public function __construct($dirName, $shardType = null)
    {
        if ($shardType === null) $shardType = CACHE_DEFAULT_SHARD;
        $this->dirName = $dirName;
        $this->root = rtrim(CACHE_ROOT, '/') . '/' . $dirName . '/';
        if (!is_dir($this->root)) mkdir($this->root, 0755, true);
        switch ($shardType) {
            case 1: $this->level1 = 1; $this->level2 = 2; break;
            case 2: $this->level1 = 1; $this->level2 = 3; break;
            case 3: $this->level1 = 2; $this->level2 = 4; break;
            default: throw new Exception("無效分片類型");
        }
    }

    // ---------- 基礎讀寫 ----------
    public function get($key)
    {
        $md5 = md5($key);
        $db = $this->getDb($md5);
        if (!$db) return false;
        try {
            $stmt = $db->prepare("SELECT content, access_count, expire_time FROM cache WHERE md5 = ?");
            $stmt->execute([$md5]);
            $row = $stmt->fetch(PDO::FETCH_ASSOC);
            if (!$row) return false;

            // 過期判斷:過期則刪除並視爲未命中
            if ($row['expire_time'] > 0 && $row['expire_time'] < time()) {
                $del = $db->prepare("DELETE FROM cache WHERE md5 = ?");
                $del->execute([$md5]);
                return false;
            }

            // 訪問計數採樣
            $prob = (int) CACHE_PROBABILITY;
            if ($prob > 0 && ($prob >= 100 || mt_rand(1, 100) <= $prob)) {
                $upd = $db->prepare("UPDATE cache SET access_count = access_count + 1 WHERE md5 = ?");
                $upd->execute([$md5]);
            }
            $raw = @gzuncompress($row['content']);
            return ($raw !== false) ? $raw : $row['content'];
        } catch (Exception $e) {
            error_log("ApiCache get error: " . $e->getMessage());
            return false;
        }
    }

    public function set($key, $data, $ttl = 0)
    {
        $md5 = md5($key);
        $db = $this->getDb($md5);
        if (!$db) return false;
        
        $expire = ($ttl > 0) ? time() + $ttl : 0;
        $compressed = (strlen($data) < 200) ? $data : gzcompress($data, 6);
        
        try {
            $stmt = $db->prepare("UPDATE cache SET content = ?, expire_time = ? WHERE md5 = ?");
            $stmt->execute([$compressed, $expire, $md5]);
            if ($stmt->rowCount() > 0) return true;
            
            $stmt = $db->prepare("INSERT INTO cache (md5, content, access_count, first_time, expire_time) VALUES (?, ?, 0, ?, ?)");
            return $stmt->execute([$md5, $compressed, time(), $expire]);
        } catch (Exception $e) {
            error_log("ApiCache set error: " . $e->getMessage());
            return false;
        }
    }

    public function delete($key)
    {
        $md5 = md5($key);
        $db = $this->getDb($md5);
        if (!$db) return false;
        try {
            $stmt = $db->prepare("DELETE FROM cache WHERE md5 = ?");
            $stmt->execute([$md5]);
            return $stmt->rowCount() > 0;
        } catch (Exception $e) {
            error_log("ApiCache delete error: " . $e->getMessage());
            return false;
        }
    }

    // ---------- 片段緩存 ----------
    public function start($key, $ttl = 0)
    {
        $content = $this->get($key);
        if ($content !== false) {
            echo $content;
            return true;
        }
        ob_start();
        $this->obStack[] = [
            'key'   => $key,
            'level' => ob_get_level(),
            'ttl'   => $ttl
        ];
        return false;
    }

    public function end()
    {
        if (empty($this->obStack)) {
            if (ob_get_level() > 0) ob_end_clean();
            return;
        }
        $info = array_pop($this->obStack);
        if (ob_get_level() != $info['level'] + 1) {
            echo ob_get_clean();
            return;
        }
        $content = ob_get_clean();
        $this->set($info['key'], $content, $info['ttl']);
        echo $content;
    }

    // ---------- 全頁面緩存 ----------
    public function page($key, $ttl = 0)
    {
        $content = $this->get($key);
        if ($content !== false) {
            echo $content;
            exit;
        }
        ob_start(function($buffer) use ($key, $ttl) {
            $this->set($key, $buffer, $ttl);
            return $buffer;
        });
    }

    // ---------- 內部工具 ----------
    private function getDb($md5)
    {
        $dir1 = substr($md5, 0, $this->level1);
        $filePrefix = substr($md5, 0, $this->level2);
        $dirPath = $this->root . $dir1;
        $dbFile = $dirPath . '/' . $filePrefix . '.db';
        $poolKey = $this->root . '|' . $dir1 . '/' . $filePrefix;
        if (isset($this->dbPool[$poolKey])) return $this->dbPool[$poolKey];

        if (!is_dir($dirPath)) {
            if (!mkdir($dirPath, 0755, true)) return false;
        }
        try {
            $pdo = new PDO("sqlite:" . $dbFile);
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            
            // 建表語句直接包含 expire_time,無需後續 ALTER
            $pdo->exec("CREATE TABLE IF NOT EXISTS cache (
                md5 TEXT PRIMARY KEY,
                content BLOB,
                access_count INTEGER DEFAULT 0,
                first_time INTEGER,
                expire_time INTEGER DEFAULT 0
            )");
            $pdo->exec("CREATE INDEX IF NOT EXISTS idx_first_time ON cache(first_time)");
            $pdo->exec("CREATE INDEX IF NOT EXISTS idx_access_count ON cache(access_count)");
            $pdo->exec("CREATE INDEX IF NOT EXISTS idx_expire_time ON cache(expire_time)");

            $pdo->exec("PRAGMA journal_mode = WAL");
            $pdo->exec("PRAGMA synchronous = OFF");
            $this->dbPool[$poolKey] = $pdo;
            return $pdo;
        } catch (PDOException $e) {
            error_log("SQLite open failed: " . $e->getMessage());
            return false;
        }
    }

    public function close() { $this->dbPool = []; }
    public function getRoot() { return $this->root; }
}

// ---------- 全局輔助函數 ----------
function get_cache_instance($dirName, $shardType = null)
{
    static $instances = [];
    if ($shardType === null) $shardType = CACHE_DEFAULT_SHARD;
    $key = $dirName . '|' . $shardType;
    if (!isset($instances[$key])) {
        $instances[$key] = new ApiCache($dirName, $shardType);
    }
    return $instances[$key];
}

// 全頁面緩存(支持 $ttl,單位:秒)
function cache_page($key, $dirName = 'Mpage', $shardType = null, $ttl = 0)
{
    if ($shardType === null) $shardType = CACHE_DEFAULT_SHARD;
    $cache = get_cache_instance($dirName, $shardType);
    $cache->page($key, $ttl);
}

// 片段緩存開始(支持 $ttl,單位:秒)
function cstart($key, $dirName = 'Mscene', $shardType = null, $ttl = 0)
{
    global $_cache_stack;
    if (!isset($_cache_stack) || !is_array($_cache_stack)) $_cache_stack = [];
    if ($shardType === null) $shardType = CACHE_DEFAULT_SHARD;
    $cache = get_cache_instance($dirName, $shardType);
    $result = $cache->start($key, $ttl);
    if (!$result) {
        $_cache_stack[] = ['dir' => $dirName, 'shard' => $shardType, 'ttl' => $ttl];
    }
    return $result;
}

function cend()
{
    global $_cache_stack;
    if (empty($_cache_stack)) {
        if (ob_get_level() > 0) ob_end_clean();
        return;
    }
    $info = array_pop($_cache_stack);
    $cache = get_cache_instance($info['dir'], $info['shard']);
    $cache->end();
}

這個類幹了啥?

  • get() 和 set() 負責對象緩存(給後面object-cache.php用)。

  • delete() 負責刪除單個緩存(當文章更新時需要清空相關緩存)。

  • cache_page() 負責全頁面緩存,命中就直接輸出HTML並退出,未命中就捕獲輸出並保存。

第二步:寫一個object-cache.php,平替Redis插件

WordPress有個特殊機制:如果/wp-content/目錄下存在object-cache.php,所有wp_cache_get()wp_cache_set()這類函數就會自動走這個文件,而不是默認的數據庫。

我們就是用這個文件,把所有對象緩存操作指向我們的SQLite類,完全繞過Redis。

保存爲/wp-content/object-cache.php

<?php
/**
 * 使用新版 ApiCache(支持 TTL)作爲 WordPress 對象緩存後端
 * 適配 expire_time 自動過期
 */

if (!defined('WP_USE_EXT_OBJECT_CACHE')) {
    define('WP_USE_EXT_OBJECT_CACHE', true);
}

// 引入你的新版 ApiCache(路徑根據實際調整)
require_once __DIR__ . '/ApiCache.php';

// 緩存根目錄(確保與 ApiCache 中一致)
if (!defined('CACHE_ROOT')) {
    define('CACHE_ROOT', WP_CONTENT_DIR . '/Mcache');
}

// 默認前綴(可留空)
if (!defined('WP_REDIS_PREFIX')) {
    define('WP_REDIS_PREFIX', '');
}

class WP_Object_Cache {
    private $apiCache;
    private $cache = [];          // 內存緩存
    private $global_groups = [
        'blog-details', 'blog-id-cache', 'blog-lookup', 'global-posts',
        'networks', 'rss', 'sites', 'site-details', 'site-lookup',
        'site-options', 'site-transient', 'users', 'useremail',
        'userlogins', 'usermeta', 'user_meta', 'userslugs'
    ];
    private $ignored_groups = [];
    private $global_prefix = '';
    private $blog_prefix = 0;
    public $cache_hits = 0;
    public $cache_misses = 0;

    public function __construct() {
        global $blog_id, $table_prefix;
        $this->global_prefix = is_multisite() ? '' : $table_prefix;
        $this->blog_prefix   = is_multisite() ? $blog_id : $table_prefix;
        // 分片模式 1(16×16),目錄 WpObject
        $this->apiCache = get_cache_instance('WpObject', 1);
    }

    private function build_key($key, $group) {
        $salt = WP_REDIS_PREFIX;
        $prefix = $this->is_global_group($group) ? $this->global_prefix : $this->blog_prefix;
        return "{$salt}{$prefix}:{$group}:{$key}";
    }

    private function is_global_group($group) {
        return in_array($group, $this->global_groups);
    }

    private function is_ignored_group($group) {
        return in_array($group, $this->ignored_groups);
    }

    // -------- 核心方法 --------
    public function get($key, $group = 'default', $force = false, &$found = null) {
        $derived_key = $this->build_key($key, $group);

        // 內存命中
        if (array_key_exists($derived_key, $this->cache) && !$force) {
            $found = true;
            $this->cache_hits++;
            return $this->cache[$derived_key];
        }

        // 忽略組或緩存不可用
        if ($this->is_ignored_group($group)) {
            $found = false;
            $this->cache_misses++;
            return false;
        }

        // 從 SQLite 讀取(get 內部會自動檢查 expire_time)
        $value = $this->apiCache->get($derived_key);
        if ($value === false) {
            $found = false;
            $this->cache_misses++;
            return false;
        }

        // 反序列化
        $data = @unserialize($value);
        if ($data === false && $value !== serialize(false)) {
            $data = $value;
        }

        $found = true;
        $this->cache_hits++;
        $this->cache[$derived_key] = $data;
        return $data;
    }

    public function set($key, $value, $group = 'default', $expiration = 0) {
        $derived_key = $this->build_key($key, $group);

        if ($this->is_ignored_group($group)) {
            $this->cache[$derived_key] = $value;
            return true;
        }

        $serialized = serialize($value);
        // 將 WordPress 的過期秒數傳給 ApiCache 的 $ttl
        $success = $this->apiCache->set($derived_key, $serialized, (int)$expiration);
        if ($success) {
            $this->cache[$derived_key] = $value;
        }
        return $success;
    }

    public function delete($key, $group = 'default', $deprecated = false) {
        $derived_key = $this->build_key($key, $group);
        unset($this->cache[$derived_key]);

        if ($this->is_ignored_group($group)) {
            return true;
        }
        return $this->apiCache->delete($derived_key);
    }

    public function add($key, $value, $group = 'default', $expiration = 0) {
        if (function_exists('wp_suspend_cache_addition') && wp_suspend_cache_addition()) {
            return false;
        }

        $derived_key = $this->build_key($key, $group);
        if (array_key_exists($derived_key, $this->cache)) {
            return false;
        }

        // 檢查持久層是否存在
        if (!$this->is_ignored_group($group)) {
            $exists = $this->apiCache->get($derived_key);
            if ($exists !== false) {
                return false;
            }
        }

        return $this->set($key, $value, $group, $expiration);
    }

    public function replace($key, $value, $group = 'default', $expiration = 0) {
        $derived_key = $this->build_key($key, $group);
        if ($this->is_ignored_group($group)) {
            if (!array_key_exists($derived_key, $this->cache)) {
                return false;
            }
        } else {
            $exists = $this->apiCache->get($derived_key);
            if ($exists === false) {
                return false;
            }
        }
        return $this->set($key, $value, $group, $expiration);
    }

    public function flush() {
        $this->cache = [];
        // 不刪除 SQLite 文件,由清理腳本管理
        return true;
    }

    public function flush_runtime() {
        $this->cache = [];
        return true;
    }

    public function increment($key, $offset = 1, $group = 'default') {
        $value = $this->get($key, $group);
        if ($value === false) {
            $value = 0;
        }
        if (!is_numeric($value)) {
            return false;
        }
        $new_value = (int)$value + $offset;
        $this->set($key, $new_value, $group);
        return $new_value;
    }

    public function decrement($key, $offset = 1, $group = 'default') {
        return $this->increment($key, -$offset, $group);
    }

    // 批量操作(簡單實現)
    public function get_multiple($keys, $group = 'default', $force = false) {
        $results = [];
        foreach ($keys as $key) {
            $results[$key] = $this->get($key, $group, $force);
        }
        return $results;
    }

    public function set_multiple($data, $group = 'default', $expire = 0) {
        $results = [];
        foreach ($data as $key => $value) {
            $results[$key] = $this->set($key, $value, $group, $expire);
        }
        return $results;
    }

    public function delete_multiple($keys, $group = 'default') {
        $results = [];
        foreach ($keys as $key) {
            $results[$key] = $this->delete($key, $group);
        }
        return $results;
    }

    public function add_multiple($data, $group = 'default', $expire = 0) {
        $results = [];
        foreach ($data as $key => $value) {
            $results[$key] = $this->add($key, $value, $group, $expire);
        }
        return $results;
    }

    // 組管理
    public function add_global_groups($groups) {
        $this->global_groups = array_unique(array_merge($this->global_groups, (array)$groups));
    }

    public function add_non_persistent_groups($groups) {
        $this->ignored_groups = array_unique(array_merge($this->ignored_groups, (array)$groups));
    }

    public function switch_to_blog($blog_id) {
        if (!function_exists('is_multisite') || !is_multisite()) {
            return false;
        }
        $this->blog_prefix = (int)$blog_id;
        return true;
    }

    // 統計信息(可選)
    public function stats() {
        echo "<p><strong>Cache Hits:</strong> {$this->cache_hits}<br />";
        echo "<strong>Cache Misses:</strong> {$this->cache_misses}</p>";
    }

    public function info() {
        return (object)[
            'hits'   => $this->cache_hits,
            'misses' => $this->cache_misses,
            'ratio'  => ($this->cache_hits + $this->cache_misses) > 0
                ? round($this->cache_hits / ($this->cache_hits + $this->cache_misses) * 100, 1)
                : 100,
            'groups' => (object)[
                'global' => $this->global_groups,
                'non_persistent' => $this->ignored_groups,
            ],
            'meta' => ['Client' => 'SQLite + ApiCache (TTL)'],
        ];
    }
}

// -------- 全局函數橋接 --------
function wp_cache_init() {
    global $wp_object_cache;
    if (!($wp_object_cache instanceof WP_Object_Cache)) {
        $wp_object_cache = new WP_Object_Cache();
    }
}

function wp_cache_add($key, $data, $group = '', $expire = 0) {
    global $wp_object_cache;
    return $wp_object_cache->add($key, $data, $group, $expire);
}
function wp_cache_set($key, $data, $group = '', $expire = 0) {
    global $wp_object_cache;
    return $wp_object_cache->set($key, $data, $group, $expire);
}
function wp_cache_get($key, $group = '', $force = false, &$found = null) {
    global $wp_object_cache;
    return $wp_object_cache->get($key, $group, $force, $found);
}
function wp_cache_delete($key, $group = '', $deprecated = 0) {
    global $wp_object_cache;
    return $wp_object_cache->delete($key, $group, $deprecated);
}
function wp_cache_replace($key, $data, $group = '', $expire = 0) {
    global $wp_object_cache;
    return $wp_object_cache->replace($key, $data, $group, $expire);
}
function wp_cache_flush() {
    global $wp_object_cache;
    return $wp_object_cache->flush();
}
function wp_cache_flush_runtime() {
    global $wp_object_cache;
    return $wp_object_cache->flush_runtime();
}
function wp_cache_incr($key, $offset = 1, $group = '') {
    global $wp_object_cache;
    return $wp_object_cache->increment($key, $offset, $group);
}
function wp_cache_decr($key, $offset = 1, $group = '') {
    global $wp_object_cache;
    return $wp_object_cache->decrement($key, $offset, $group);
}
function wp_cache_get_multiple($keys, $group = '', $force = false) {
    global $wp_object_cache;
    return $wp_object_cache->get_multiple($keys, $group, $force);
}
function wp_cache_set_multiple($data, $group = '', $expire = 0) {
    global $wp_object_cache;
    return $wp_object_cache->set_multiple($data, $group, $expire);
}
function wp_cache_delete_multiple($keys, $group = '') {
    global $wp_object_cache;
    return $wp_object_cache->delete_multiple($keys, $group);
}
function wp_cache_add_multiple($data, $group = '', $expire = 0) {
    global $wp_object_cache;
    return $wp_object_cache->add_multiple($data, $group, $expire);
}
function wp_cache_add_global_groups($groups) {
    global $wp_object_cache;
    $wp_object_cache->add_global_groups($groups);
}
function wp_cache_add_non_persistent_groups($groups) {
    global $wp_object_cache;
    $wp_object_cache->add_non_persistent_groups($groups);
}
function wp_cache_switch_to_blog($blog_id) {
    global $wp_object_cache;
    return $wp_object_cache->switch_to_blog($blog_id);
}
function wp_cache_supports($feature) {
    switch ($feature) {
        case 'add_multiple':
        case 'set_multiple':
        case 'get_multiple':
        case 'delete_multiple':
        case 'flush_runtime':
            return true;
        default:
            return false;
    }
}
function wp_cache_close() {
    return true;
}

// 自動初始化
wp_cache_init();

第三步:修改根目錄index.php,給遊客加全頁緩存

這個最簡單,直接在網站根目錄的index.php最前面加上:

<?php
/**
 * Front to the WordPress application – 加了全頁緩存加速(帶排除規則)
 */

// ---------- 加載 WordPress 核心(僅用於判斷登錄狀態) ----------
require_once __DIR__ . '/wp-load.php';

// ---------- 定義不需要緩存的 URI 列表(支持部分匹配) ----------
$excluded_uris = [
    '/qq-login/',      // 你的 QQ 登錄頁面路徑
    '/oauth/',         // 通用的 OAuth 回調路徑
    '/wp-login.php',   // WordPress 登錄頁
    '/wp-admin/',      // 後臺管理(雖然通常不走 index.php,但以防萬一)
    '/register/',      // 註冊頁面
    '/checkout/',      // 支付/結賬頁面(如果有)
    // 你可以繼續添加更多需要排除的路徑
];

// 檢查當前請求 URI 是否匹配排除列表
$current_uri = $_SERVER['REQUEST_URI'];
$skip_cache = false;
foreach ($excluded_uris as $uri) {
    if (strpos($current_uri, $uri) !== false) {
        $skip_cache = true;
        break;
    }
}

// 如果是登錄用戶,也跳過緩存(因爲要顯示個性化內容)
if (is_user_logged_in()) {
    $skip_cache = true;
}

// ---------- 遊客且未排除,才使用全頁緩存 ----------
if (!$skip_cache && !is_user_logged_in()) {
    // 引入你的 ApiCache 類
    require_once __DIR__ . '/wp-content/ApiCache.php';

    // 生成緩存鍵(基於當前 URL,區分 GET 參數)
    $cache_key = 'page_' . md5($_SERVER['REQUEST_URI']);

    // 調用全頁緩存函數(命中則輸出並退出,否則開始捕獲)
    cache_page($cache_key, 'Mpage', 1, 86400);
    // 注意:cache_page 若命中會 exit,若未命中則會繼續執行下面的代碼
}

// ---------- 正常 WordPress 加載 ----------
define('WP_USE_THEMES', true);
require __DIR__ . '/wp-blog-header.php';

就這幾行,遊客訪問時直接輸出SQLite裏的HTML,不再執行主題的PHP代碼。

第四步:定期清理過期緩存(獨立腳本)

SQLite文件不會自動過期,所以我們需要一個腳本定時清理低頻數據。我寫了一個ClearCache.php,放在命令行裏跑(比如每天凌晨):

#!/usr/bin/env php
<?php
// ClearCache.php - 新版清理:過期刪除 + 超限冷淘汰
// 適配你的實際路徑

if (php_sapi_name() !== 'cli') {
    die("僅允許 CLI 運行\n");
}

// ---------- 配置 ----------
define('CACHE_ROOT', '/www/wwwroot/你的網站/wp-content/緩存主路徑'); // 請修改爲你的實際路徑

// 清理任務列表
$CLEAN_TASKS = [
    [
        'path'        => CACHE_ROOT . '/WpObject',
        'max_records' => 2000,    // 對象緩存最多 20 萬條
    ],
    [
        'path'        => CACHE_ROOT . '/Mpage',
        'max_records' => 2000,     // 全頁緩存最多 5 萬條
    ],
    // 如果有其他緩存目錄(如 default、Mlyric 等)可繼續添加
];

// ---------- 執行 ----------
echo "[" . date('Y-m-d H:i:s') . "] 開始清理(過期刪除 + 超限冷淘汰)...\n";

$totalExpired = 0;
$totalOverLimit = 0;

foreach ($CLEAN_TASKS as $task) {
    $path = $task['path'];
    echo "\n--- 清理任務: $path ---\n";
    if (!is_dir($path)) {
        echo "  目錄不存在,跳過\n";
        continue;
    }

    $dbFiles = getDbFiles($path);
    if (empty($dbFiles)) {
        echo "  沒有找到 .db 文件\n";
        continue;
    }
    echo "  發現 " . count($dbFiles) . " 個 .db 文件\n";

    // 第一步:刪除所有已過期的記錄
    $deletedExpired = 0;
    $fileCount = 0;
    foreach ($dbFiles as $dbFile) {
        $fileCount++;
        $deleted = cleanExpiredRecords($dbFile);
        if ($deleted > 0) {
            echo "  過期清理 $dbFile : 刪除 $deleted 條\n";
        }
        $deletedExpired += $deleted;
        if ($fileCount % 10 == 0) {
            echo "  已處理 $fileCount 個文件...\n";
        }
    }
    echo "第一步完成,共刪除過期緩存 $deletedExpired 條\n";
    $totalExpired += $deletedExpired;

    // 第二步:如果總數仍超限,按訪問量刪除最冷數據
    $totalRecords = countAllRecords($dbFiles);
    $max = $task['max_records'];
    if ($totalRecords > $max) {
        $need = $totalRecords - $max;
        echo "記錄數 $totalRecords 超過限制($max),需刪除 $need 條最冷數據\n";
        $deletedCold = deleteColdestRecords($dbFiles, $need);
        echo "第二步刪除 $deletedCold 條\n";
        $totalOverLimit += $deletedCold;
    } else {
        echo "記錄數 $totalRecords 未超限 $max\n";
    }

    // 清理空目錄
    removeEmptyDirs($path, false);
}

echo "\n[" . date('Y-m-d H:i:s') . "] 清理完成\n";
echo "彙總:過期刪除 $totalExpired 條,超限冷淘汰 $totalOverLimit 條\n";

// ---------- 輔助函數 ----------
function getDbFiles($root) {
    $files = [];
    if (!is_dir($root)) return $files;
    $iter = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($root, RecursiveDirectoryIterator::SKIP_DOTS)
    );
    foreach ($iter as $file) {
        if ($file->isFile() && $file->getExtension() === 'db') {
            $files[] = $file->getPathname();
        }
    }
    return $files;
}

function cleanExpiredRecords($dbFile) {
    try {
        $pdo = new PDO("sqlite:" . $dbFile);
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $now = time();
        $limit = 1000;
        $lastMd5 = '';
        $totalDeleted = 0;

        do {
            $stmt = $pdo->prepare("SELECT md5, expire_time FROM cache WHERE md5 > ? ORDER BY md5 LIMIT ?");
            $stmt->execute([$lastMd5, $limit]);
            $rows = $stmt->fetchAll();
            if (empty($rows)) break;

            $toDelete = [];
            foreach ($rows as $row) {
                if ($row['expire_time'] > 0 && $row['expire_time'] < $now) {
                    $toDelete[] = $row['md5'];
                }
            }
            if (!empty($toDelete)) {
                $in = implode(',', array_fill(0, count($toDelete), '?'));
                $del = $pdo->prepare("DELETE FROM cache WHERE md5 IN ($in)");
                $del->execute($toDelete);
                $totalDeleted += $del->rowCount();
            }
            $lastMd5 = end($rows)['md5'];
        } while (true);

        unset($pdo);
        return $totalDeleted;
    } catch (Exception $e) {
        error_log("清理失敗 $dbFile: " . $e->getMessage());
        return 0;
    }
}

function countAllRecords($dbFiles) {
    $total = 0;
    foreach ($dbFiles as $dbFile) {
        try {
            $pdo = new PDO("sqlite:" . $dbFile);
            $total += (int) $pdo->query("SELECT COUNT(*) FROM cache")->fetchColumn();
            unset($pdo);
        } catch (Exception $e) {
            // 忽略
        }
    }
    return $total;
}

function deleteColdestRecords($dbFiles, $need) {
    $dbStats = [];
    $totalRecords = 0;
    foreach ($dbFiles as $dbFile) {
        try {
            $pdo = new PDO("sqlite:" . $dbFile);
            $count = (int) $pdo->query("SELECT COUNT(*) FROM cache")->fetchColumn();
            $dbStats[$dbFile] = $count;
            $totalRecords += $count;
            unset($pdo);
        } catch (Exception $e) {
            // 忽略
        }
    }

    if ($totalRecords == 0) return 0;

    $deleted = 0;
    foreach ($dbStats as $dbFile => $count) {
        if ($count == 0) continue;
        $deleteCount = (int) ceil($need * ($count / $totalRecords));
        if ($deleteCount <= 0) continue;

        try {
            $pdo = new PDO("sqlite:" . $dbFile);
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $del = $pdo->prepare("DELETE FROM cache ORDER BY access_count ASC, first_time ASC LIMIT ?");
            $del->execute([$deleteCount]);
            $deleted += $del->rowCount();
            unset($pdo);
        } catch (Exception $e) {
            // 忽略
        }
    }

    // 補刪
    if ($deleted < $need) {
        $remaining = $need - $deleted;
        foreach ($dbFiles as $dbFile) {
            if ($remaining <= 0) break;
            try {
                $pdo = new PDO("sqlite:" . $dbFile);
                $del = $pdo->prepare("DELETE FROM cache ORDER BY access_count ASC, first_time ASC LIMIT 1");
                $del->execute();
                if ($del->rowCount() > 0) {
                    $deleted++;
                    $remaining--;
                }
                unset($pdo);
            } catch (Exception $e) {
                // 忽略
            }
        }
    }

    return $deleted;
}

function removeEmptyDirs($dir, $isRoot = true) {
    if (!is_dir($dir)) return;
    foreach (scandir($dir) as $item) {
        if ($item === '.' || $item === '..') continue;
        $path = $dir . DIRECTORY_SEPARATOR . $item;
        if (is_dir($path)) {
            removeEmptyDirs($path, false);
        }
    }
    if (!$isRoot && count(scandir($dir)) == 2) {
        @rmdir($dir);
    }
}

至此。
可以實現飛速加載頁面。
當然,如果網站每天更新內容,可以不整頁緩存。

微信赞赏

微信

支付宝赞赏

支付寶

✍️ 作者: 紅穆

網站管理員 · 感謝閱讀,更多精彩內容敬請關注

作者主頁 查看主頁 →

相關文章

wordpress怎么取消修订和自动保存草稿?

wordpress怎麼取消修訂和自動保存草稿? 程序筆記 wordpress

WordPress自動儲存文章修訂版本記錄的功能,當你每編輯一次文章時,就會在後臺記錄一次,每次修訂都會在posts表中插入一條記錄。由於文章修訂與自動保存的干預,往往會使得文章的ID越來越大。雖然不會對於你的Wordpress運行造成很大的麻煩,但是文章版本過多的話會給你的空間和數據庫增加很大的負…
👁 372
帝国cms博客数据迁移至wordpress详细教程

帝國cms博客數據遷移至wordpress詳細教程 程序筆記 wordpress

PS1:第二次修改,目前已支持分類,文章,tag三者數據轉移PS2:如果文章ID一直對不齊,可以嘗試清空WordPress的文章分類數據表,關係表我之前博客用typecho,後來遷移到帝國,但是帝國功能太多了,博客用帝國有點大材小用的感覺!再者,我用博客我常記錄一些重要的代碼,這些代碼都是我辛辛苦苦…
👁 546
wordpress关于图片转码导致页面卡死的问题

wordpress關於圖片轉碼導致頁面卡死的問題 程序筆記 wordpress

昨晚上在上傳圖片的時候,不知道怎麼回事,然後上傳的圖片自動轉碼了轉碼類型:BASE64編碼網站上面一張圖,你放服務器上,然後訪問鏈接,這樣圖片就顯示了但還有一種辦法,那就是直接將圖片轉碼,這樣直接訪問轉換好的CODE便可以訪問問題描述:當圖片比較小,只有幾kb的時候,轉碼然後設置在網站上,這樣可以避…
👁 152
wordpress 数据库查询操作

wordpress 數據庫查詢操作 程序筆記 wordpress

首先鏈接數據庫要連接WordPress數據庫,需要在PHP文件中引入wp-config.php文件。該文件包含有關WordPress安裝的數據庫配置信息和其他常量。以下是一個簡單的示例,展示如何引入wp-config.php文件並獲取數據庫連接:// 引入 wp-config.php 文件 requ…
👁 377

推薦閱讀

英语教育培训机构网站模板 0376

英語教育培訓機構網站模板 0376 實用收藏 易優模板

一款針對英語教育培訓機構的eyoucms網站模板。設計風格國際教育,能夠展示英語培訓課程、師資力量、教學成果及留學服務。有助於英語培訓機構在線上吸引學生,提升品牌知名度。模板展示 安裝說明 網站後臺:/login.php 賬號:admin 密碼:admin 相關文章易優CMS安裝常見問題總結易優CM…
👁 58
(自适应手机端)微商货源网站pbootcms模板 网店微商代理网站模板下载 0348

(自適應手機端)微商貨源網站pbootcms模板 網店微商代理網站模板下載 0348 實用收藏 pbootcms模板

一款自適應手機端的微商貨源與網店代理PbootCMS網站模板。設計風格實用信息流,適合展示微商產品、代理政策及貨源信息。有助於微商平臺或貨源網站在線上吸引代理與分銷商。模板展示 安裝說明 網站後臺:/admin.php 賬號:admin 密碼:admin 解壓密碼:www.4s5.cn相關文章Pbo…
👁 48
烹饪餐饮小吃培训学校网站模板 0138

烹飪餐飲小喫培訓學校網站模板 0138 實用收藏 易優模板

一款針對烹飪、餐飲小喫及培訓學校的eyoucms網站模板。設計風格美食教育,能夠展示烹飪課程、小喫培訓、師資力量及學員作品。有助於餐飲培訓機構在線上吸引學員,提升品牌知名度。模板展示 安裝說明 網站後臺:/login.php 賬號:admin 密碼:admin 相關文章易優CMS安裝常見問題總結易優…
👁 43
(PC+WAP)蓝色大气机电机械设备制造类企业网站pbootcms模板 机械设备网站源码下载 0108

(PC+WAP)藍色大氣機電機械設備製造類企業網站pbootcms模板 機械設備網站源碼下載 0108 實用收藏 pbootcms模板

一款藍色大氣的機電機械設備製造類企業PbootCMS網站模板,支持PC與WAP端訪問。設計風格專業科技,適合展示機電一體化產品與工業解決方案。模板內置產品中心與新聞動態模塊,有助於機械設備公司在線上高效獲客與品牌推廣。模板展示 安裝說明 網站後臺:/admin.php 賬號:admin 密碼:adm…
👁 43
网页禁止ireame嵌套代码

網頁禁止ireame嵌套代碼 踩坑總結

js方法: &lt;script&nbsp;type=&quot;text/javascript&quot;&gt; if(self&nbsp;!=&nbsp;top)&nbsp;{&nbsp;top.location&nbsp;=&nbsp;self.location;&nbsp;} &lt;/s…
👁 133
响应式网络设计广告设计公司网站模板 1124

響應式網絡設計廣告設計公司網站模板 1124 實用收藏 易優模板

此套eyoucms響應式模板適用於網絡設計與廣告設計公司,設計風格創意專業,能夠展示網站設計、廣告創意、品牌視覺及案例作品。有助於設計公司在線上展示實力,吸引企業客戶。模板展示 安裝說明 網站後臺:/login.php 賬號:admin 密碼:admin 相關文章易優CMS安裝常見問題總結易優CMS…
👁 45