如何让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);
    }
}

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

© 版权声明
THE END
喜欢就支持一下吧
点赞5分享
评论 抢沙发

请登录后发表评论

    暂无评论内容