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:
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.
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.