紅穆ノート
レコメンド プログラムノート WordPressをスムーズに運用するにはどうすればよいでしょうか?私はSQLiteを用いてRedisを置き換えました。
プログラムノート 落とし穴のまとめ wordpress

WordPressをスムーズに運用するにはどうすればよいでしょうか?私はSQLiteを用いてRedisを置き換えました。

WordPressをスムーズに運用するにはどうすればよいでしょうか?私はSQLiteを用いてRedisを置き換えました。

当サイトは以下で稼働しています:WordPress + Subbi Themeこのアプリと比べれば、理解している人なら誰でもわかるでしょう――ログイン、会員登録、ダウンロード権限、QQログイン、返信可視化など――機能がぎっしりと詰め込まれており、毎回ページを更新するたびに数十回もデータベースを検索する必要があります。

そこで、これを簡単な改造してみようと思いました。

コードはすでに配置されています。
https://github.com/1810216796/wordpress-sqlite-cache/

この方案は一体どのような目的で策定されたのでしょうか?

核心的な考え方は以下の2点です:

  1. オブジェクトキャッシュRedisの代わりに、データベースからのクエリ結果(例えば記事データやユーザー情報など)をSQLiteファイルに保存します。WordPressは毎回クエリを実行する前にまずSQLiteに照会し、存在する場合は直接取得することで、MySQLのIO操作を省略します。

  2. 全ページ静的キャッシュ(Page Cache)ログインしていない訪問者向けに、ページ全体のHTMLを直接出力します。これにより、PHPテンプレートのレンダリング処理も省けます。これは、動的サイトを純粋な静的サイトに変装する効果があります。

この2層構成により、わずか99元で利用できるサーバーでも、「高額サーバー」と同じような高性能感を実現できます。

ステップ1:ApiCache.phpRedisの代わりに使用する処理

この記事:高効率なWebページキャッシュ用ページコード

このクラスは担当します。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() &nbsp;と&nbsp; set() 対象データのキャッシュ管理(後続処理用)object-cache.php使用)。

  • delete() 単一のキャッシュを削除する(記事の更新時などに関連するキャッシュをクリアする必要がある)。

  • cache_page() 全ページのキャッシュ処理を担当します。キャッシュに命中した場合、直接HTMLを出力して処理を終了します。キャッシュに命中しなかった場合、出力内容をキャプチャし、保存します。

ステップ2:一つ書くobject-cache.phpRedisプラグインの代替品

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代码。

第4段階:期限切れのキャッシュを定期的にクリーンアップする(独立したスクリプト)

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);
    }
}

至此。
可以实现飞速加载页面。
当然,如果网站每天更新内容,可以不整页缓存。

微信赞赏

WeChat

支付宝赞赏

Alipay

✍️ クリエイター: 紅穆

ウェブサイト管理者 · ご覧いただきありがとうございます!さらに多くの魅力的なコンテンツをお楽しみいただけますよう、引き続きご支援をお願いいたします。

著者プロフィール プロフィールを見る →

関連記事

WordPressでRedisを起動した後、パスワードを設定した結果、Redisへの接続が不可能となり、Redisにアクセスできません:「`SELECT` failed:NOAUTH Authentication required」

WordPressでRedisを起動した後、パスワードを設定した結果、Redisへの接続が不可能となり、Redisにアクセスできません:「`SELECT` failed:NOAUTH Authentication required」 プログラムノート 落とし穴のまとめ wordpress

これはインターネット上で最も多く参照されている記事で、Redisに関する問題の80%を解決できます!WordPressでRedisのパスワードを変更した後、Redis Object Cacheプラグインが接続できない場合、どう対処すればよいでしょうか?もちろん、残りの20%のケースでは、wp-config.phpファイルを編集する必要があります!解決策として、以前はRedisプラグインのobject-cache.phpファイルにredi…を追加していました。
👁 611
WordPressでRedisのパスワードを変更した後、Redis Object Cacheプラグインが接続できない場合、どう対処すればよいですか?

WordPressでRedisのパスワードを変更した後、Redis Object Cacheプラグインが接続できない場合、どう対処すればよいですか? プログラムノート wordpress

問題の説明:当方はWordPressサイトのサーバーに「BaoTa Panel」を使用しており、PHP 8.0をインストールし、拡張機能パネルからRedisを導入し、WordPressに「Redis Object Cache」プラグインをインストールしています。通常は特別な設定を加えていなくても、そのまま起動すれば問題ありません。しかし、Redisにパスワードを設定しない場合、セキュリティ面で非常に脆弱であり、リスクのある脆弱性となります。そのため、パスワードを設定することを推奨します。…
👁 262
WordPressで修正履歴や草稿の自動保存を無効にするにはどうすればよいですか?

WordPressで修正履歴や草稿の自動保存を無効にするにはどうすればよいですか? プログラムノート wordpress

WordPressの「記事の変更履歴を自動保存」機能により、記事を編集するたびにバックエンドで変更履歴が記録され、各変更ごとに「posts」テーブルに1件のレコードが挿入されます。この機能により、記事のIDが次第に大きくなることがよくあります。この状態がWordPressの運用に大きな支障をきたすことはありませんが、記事のバージョン数が多すぎると、サーバーのストレージ容量やデータベース負荷が大幅に増加する可能性があります。
👁 379
EmpireCMSからWordPressへのブログデータ移行の詳細なチュートリアル

EmpireCMSからWordPressへのブログデータ移行の詳細なチュートリアル プログラムノート wordpress

PS1:第2回修正版。現在、カテゴリ、記事、タグのデータ転送がサポートされています。PS2:もし記事IDが常に一致しない場合は、WordPressの記事カテゴリデータテーブルや関係性テーブルをクリアしてみるのも一つの方法です。以前はTypechoを使用していたブログですが、後にEmpireに移行しました。しかし、Empireには機能が多すぎます。ブログとして使用する場合、これは「大材小用」と感じます。さらに、このブログでは、重要なコードを頻繁に記録しています。これらのコードはすべて、私自身が多大な労力と時間を使って作成したものです…
👁 556
WordPressにおける画像の変換によってページが処理停止する問題

WordPressにおける画像の変換によってページが処理停止する問題 プログラムノート wordpress

昨夜、画像をアップロードしていた際、理由は不明ですが、アップロードした画像が自動的に変換されました。変換方式:BASE64符号化。ウェブサイト上に掲載されている画像をサーバー上に保存し、そのリンクからアクセスすれば、画像が表示されます。しかし、別の方法として、画像を直接変換してから、変換後のCODEを直接アクセスする方法もあります。問題の説明:画像が非常に小さく、数KB程度の場合、「変換後、ウェブサイトに掲載する」という方法を採用することで、問題を回避できる可能性があります…
👁 159
WordPressデータベースクエリ操作

WordPressデータベースクエリ操作 プログラムノート wordpress

まず、データベースに接続するには、WordPressのデータベースに接続する必要があります。そのため、PHPファイル内に `wp-config.php` ファイルをインポートする必要があります。このファイルには、WordPressのインストールに必要なデータベース設定情報やその他の定数が含まれています。以下は、`wp-config.php` ファイルをインポートし、データベース接続を取得する方法を示す簡単な例です:// `wp-config.php` ファイルをインポートする:`require…`
👁 387

おすすめ読書

EmpireCMSにおける関連検索呼び出しの最適化

EmpireCMSにおける関連検索呼び出しの最適化 プログラムノート EmpireCMS

帝国システムにおいて、新しいアイデアを検討しています。すべての記事のキーワードを格納するための別テーブルを追加し、関連記事のIDを一括して集約する方法を採用します。これは一種の「検索集約」機能に相当します。この方法の利点は、記事内から指定されたキーワードに関連する記事を簡単に検索できる点にあります。これにより、帝国システムに標準搭載されている関連検索集約機能を不要にできるのです。なぜなら、この標準機能は処理速度が非常に遅いためです。(SQLの不完全一致検索を採用しており、データ量が10万件を超える場合、サーバーの性能が低くても…)
👁 354
PHPを使用して、指定されたディレクトリから別の指定されたディレクトリへコピーする(套娃)

PHPを使用して、指定されたディレクトリから別の指定されたディレクトリへコピーする(套娃) 言語ノート PHP

ユーザーが特定のディレクトリを別の指定されたディレクトリにコピーする場合、以下の PHP 関数を使用できます:`function copyDirectory($src, $dst) { // 元のディレクトリが存在するか、かつディレクトリであるかを確認する if (!is_dir($src)) { return false; } // 目的のディレクトリが存在するか、かつディレクトリであるかを確認する …`
👁 1659
(モバイル対応版)政府党史学習用PbootCMSサイトテンプレート|「赤色教育・党建設」専門サイトソースコードのダウンロード|0178

(モバイル対応版)政府党史学習用PbootCMSサイトテンプレート|「赤色教育・党建設」専門サイトソースコードのダウンロード|0178 実用収集 pbootcmsテンプレート

モバイル対応の政府機関向け「党の歴史学習および赤色教育(党建)」専門ウェブサイト用PbootCMSテンプレートです。デザインスタイルは荘重な赤色を採用しており、党の建設に関する宣伝要件に適しています。党の歴史資料、学習活動、および党建の成果を効果的に紹介できるため、基層党組織や政府機関が専門的な学習プラットフォームを構築する際の優れた選択肢です。テンプレート表示・インストール手順:ウェブサイトの管理コンソール:/admin.php|ユーザー名:admin|パスワード:admin|解凍用パスワード:…
👁 36
(自适应手机端)蓝色大气化工滤料石材厂家pbootcms模板 磨料生产网站源码下载 0403

(自适应手机端)蓝色大气化工滤料石材厂家pbootcms模板 磨料生产网站源码下载 0403 実用収集 pbootcmsテンプレート

本套自适应手机端的蓝色大气化工滤料与磨料生产PbootCMS网站模板。设计风格专业稳重,适合展示化工滤料、磨料产品及工业应用。有助于化工材料企业在线上展示生产实力,吸引工业客户。模板展示 安装说明 网站后台:/admin.php 账号:admin 密码:admin 解压密码:www.4s5.cn相关…
👁 44
(自适应移动端)响应式外国语学校网站源码 HTML5响应式大学学校院校类网站pbootcms模板 0740

(自适应移动端)响应式外国语学校网站源码 HTML5响应式大学学校院校类网站pbootcms模板 0740 実用収集 pbootcmsテンプレート

一款响应式外国语学校与大学院校PbootCMS网站模板源码,支持移动端访问。设计风格国际教育,适合外国语大学、国际学校展示教学环境与招生信息。有助于教育机构在线上吸引海内外学生。模板展示 安装说明 网站后台:/admin.php 账号:admin 密码:admin 解压密码:www.4s5.cn相关…
👁 68