Hongmu Notes
Home Program Notes How to make WordPress fly? I used SQLite to defeat Redis
Program Notes Summary of pitfalls wordpress

How to make WordPress fly? I used SQLite to defeat Redis

How to make WordPress fly? I used SQLite to defeat Redis

My website is running onWordPress + Subbi ThemeThis app – if you know what I mean – features everything you could possibly need: login, membership, download permissions, QQ login, reply visibility, and more... The app is packed with features; every time you refresh it, it queries the database dozens of times.

So, I decided to make a simple modification to it.

The code has been uploaded.
https://github.com/1810216796/wordpress-sqlite-cache/

What exactly did this solution do?

There are two core principles:

  1. Object CacheAs an alternative to Redis, this approach stores database query results (e.g., article data or user information) in an SQLite file. Before performing any query, WordPress first checks the SQLite database; if the data exists, it is retrieved directly, thereby eliminating MySQL I/O overhead.

  2. Full-page static caching (Page Cache)For unauthenticated visitors, the entire page's HTML is output directly—eliminating the need for PHP template rendering—effectively disguising a dynamic website as a purely static one.

With these two layers added, even a server priced at just ¥99 can give you the feeling of using a "luxury server."

First Steps:ApiCache.phpSubstitute for Redis tasks

this essay:Highly efficient web page caching page code

This class is responsible.read-writeSQLite,supportMD5 Sharding(Distribute the cache across multiple files; store it within the DB file to prevent any single file from becoming too large), also includesaccess countFacilitates the subsequent cleanup of cold data.

Paste the code directly and save it as/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();
}

What does this class do?

  • get()  ; set() Responsible for object caching (for subsequent processing).object-cache.phpneed).

  • delete() Responsible for deleting individual caches (when an article is updated, the relevant caches need to be cleared).

  • cache_page() Responsible for full-page caching: if a cache hit occurs, HTML is directly output and the process exits; if a cache miss occurs, the output is captured and saved.

Step 2: Write one.object-cache.phpAlternative Redis plugin

WordPress has a special mechanism: if/wp-content/Exists in the directoryobject-cache.phpAllwp_cache_get()wp_cache_set()This function will automatically use this file instead of the default database.

We are using this document to...All object caching operationsRefers to our SQLite class, completely bypassing Redis.

Save as/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();

Step 3: Modify the root directoryindex.phpEnable full-page caching for visitors

This is the simplest method – just place it directly in the website's root directory.index.phpAdd at the beginning:

<?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';

With just these few lines of code, the HTML is directly outputted to the browser by the visitor from the SQLite database – without executing any PHP code from the theme.

Step 4: Regularly清理 expired cache (独立脚本)

SQLite files do not expire automatically, so we need a script to periodically clean out infrequently accessed data. I've written one.ClearCache.phpRun it from the command line (e.g., every morning at midnight):

#!/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);
    }
}

come here.
Enables rapid page loading.
Of course, if a website updates its content daily, page caching may not be necessary.

微信赞赏

WeChat

支付宝赞赏

Alipay

✍️ Author: Hong Mu

webmaster · Thanks for reading, stay tuned for more exciting content

Author homepage View home page →

Related articles

After installing Redis on WordPress, setting a password prevented Redis from connecting; as a result, Redis could not be accessed: `SELECT` Failed: NOAUTH – Authentication required.

After installing Redis on WordPress, setting a password prevented Redis from connecting; as a result, Redis could not be accessed: `SELECT` Failed: NOAUTH – Authentication required. Program Notes Summary of pitfalls wordpress

This is the most widely used article online – it can solve 80% of Redis-related issues! What should you do if the Redis Object Cache plugin fails to connect after you have changed the Redis password in WordPress? Of course, the remaining 20% of cases require making adjustments directly in the wp-config.php file! Solution: Previously, I added Redis-related code to the object-cache.php file of the Redis plugin...
👁 603
What should I do if the Redis Object Cache plug-in cannot be linked after wordpress redis changes the password?

What should I do if the Redis Object Cache plug-in cannot be linked after wordpress redis changes the password? Program Notes wordpress

Problem description: My wordpress site server uses pagoda panel, installed php8.0, and installed redis on the extension panel, and installed Redis Object Cache plug-in in wordpress. In fact, in general, you don't need to do anything, just start it directly. However, it is unsafe for radis not to set a password, which is a risky loophole, so I still think it is better to set a password, …
👁 255
How do I disable revisions and automatic draft saving in WordPress?

How do I disable revisions and automatic draft saving in WordPress? Program Notes wordpress

WordPress's automatic article revision tracking feature logs every time you edit an article in the后台; each revision is recorded as a separate entry in the `wp_posts` table. Due to the interplay between article revisions and automatic saving, the article ID often grows larger over time. While this typically does not cause significant issues for your WordPress installation, an excessive number of article versions can place a considerable burden on your storage space and database...
👁 372
Detailed Tutorial on Migrating EmpireCMS Blog Data to WordPress

Detailed Tutorial on Migrating EmpireCMS Blog Data to WordPress Program Notes wordpress

PS1: Second revision – now supports data transfer between categories, posts, and tags. PS2: If the post IDs consistently don't match, you can try clearing the WordPress post category data table and relationship tables. I previously used Typecho for my blog, but later migrated to Empire; however, Empire has too many features – using it for a blog feels like putting a large hammer into a small hole! Additionally, I often record important code snippets in my blog – all of which I've painstakingly written...
👁 546
WordPress issue: pages freezing due to image transcoding

WordPress issue: pages freezing due to image transcoding Program Notes wordpress

Last night, while uploading an image, for some reason, the uploaded image was automatically transcoded. Transcoding type: BASE64 encoding. For an image on a website, simply upload it to your server and then access the link – the image will then be displayed. However, there is another approach: directly transcoding the image; then you can simply access the converted image using its encoded URL. Problem description: When an image is relatively small (e.g., only a few KB in size), transcoding it and then hosting it on a website can help avoid...
👁 152
WordPress database query operations

WordPress database query operations Program Notes wordpress

To connect to a database for a WordPress installation, you need to include the `wp-config.php` file in your PHP script. This file contains the database configuration details for WordPress installation and other constants. Here is a simple example demonstrating how to include the `wp-config.php` file and establish a database connection: // Include the wp-config.php file: require...
👁 377

Recommended reading

English Language Education Training Institution Website Template 0376

English Language Education Training Institution Website Template 0376 Practical Collection Yiyou template

An EyouCMS website template designed specifically for English language education and training institutions. Its international education-themed design enables effective presentation of English training courses, faculty expertise, teaching outcomes, and overseas study services. This template helps English language training institutions attract students online and enhance their brand awareness. Template Demo | Installation Instructions | Website Backend: /login.php | Username: admin | Password: admin | Related Articles: Summary of Common Installation Issues for EyouCMS | EyouCMS...
👁 58
(Adaptive mobile version) PBootCMS template for micro-business supply chain websites – Download e-commerce and micro-business agency website templates – 0348

(Adaptive mobile version) PBootCMS template for micro-business supply chain websites – Download e-commerce and micro-business agency website templates – 0348 Practical Collection pbootcms Template

An adaptive mobile-friendly PbootCMS website template designed for WeChat business sellers and online store agents, featuring a practical information flow layout ideal for displaying WeChat business products, agent policies, and supply chain information. This template helps WeChat business platforms or supply chain websites attract agents and distributors online. Template Preview | Installation Instructions | Website Backend: /admin.php | Username: admin | Password: admin | Extraction Password: www.4s5.cn | Related Articles: Pbo...
👁 48
Culinary and Food Service Training School Website Template 0138

Culinary and Food Service Training School Website Template 0138 Practical Collection Yiyou template

An eyouCMS website template designed for cooking schools, catering and snack businesses, as well as training institutions. Its food and culinary education-themed design enables effective display of cooking courses, snack preparation training programs, faculty expertise, and student projects. This template helps catering training organizations attract learners online and enhance their brand awareness. Template Overview | Installation Instructions | Website Backend: /login.php | Username: admin | Password: admin | Related Articles: Summary of Common Installation Issues for eyouCMS | eyou...
👁 43
(PC+WAP) Blue Atmospheric Electromechanical Equipment Manufacturing Company Website – PBootCMS Template; Download Mechanical Equipment Website Source Code – 0108

(PC+WAP) Blue Atmospheric Electromechanical Equipment Manufacturing Company Website – PBootCMS Template; Download Mechanical Equipment Website Source Code – 0108 Practical Collection pbootcms Template

A sophisticated blue PbootCMS website template designed for mechatronic machinery manufacturing companies, supporting both PC and WAP access. Its professional, tech-oriented design is ideal for showcasing mechatronic products and industrial solutions. The template includes built-in Product Center and News & Updates modules, helping machinery companies efficiently acquire online customers and promote their brands. Template Display | Installation Instructions | Website Backend: /admin.php | Username: admin | Password: adm...
👁 43
Responsive Web Design – Advertising Design Agency Website Template 1124

Responsive Web Design – Advertising Design Agency Website Template 1124 Practical Collection Yiyou template

This EyouCMS responsive template is ideal for web design and advertising agencies. Its creative and professional design style is perfect for showcasing website designs, advertising creatives, brand visuals, and case studies. It helps design firms demonstrate their expertise online and attract corporate clients. Template Display | Installation Instructions | Website Backend: /login.php | Username: admin | Password: admin | Related Articles: Summary of Common Installation Issues for EyouCMS | EyouCMS...
👁 45