WordPress使用基于APCU的对象缓存

WordPress中插件提供的对象缓存感觉使用体验都一般,有一些可能还需要订阅制才能使用高级功能,其实PHP本身带了一个apcu扩展来实现基于内存的缓存,这里记录一下具体实现。

<?php
/**
 * Plugin Name: APCu Object Cache
 * Plugin URI: https://example.com/apcu-object-cache
 * Description: 使用APCu扩展的WordPress对象缓存插件,支持AdminBar统计和刷新,强制使用HTTP_HOST前缀防跨站/跨实例混淆
 * Version: 1.0.0
 * Author: Ian Zhi
 * License: GPL v2 or later
 * 
 * 注意:使用前请确保服务器已安装并启用APCu扩展
 * 默认缓存键前缀强制使用 $_SERVER['HTTP_HOST'],多站点时在其后追加 Blog ID
 */

// 防止直接访问文件
if ( !defined('ABSPATH') ) {
    exit;
}

/**
 * APCu对象缓存实现类
 */
class APCu_Object_Cache {
    
    /**
     * 缓存统计信息
     * @var array
     */
    private $stats = [
        'get' => 0,
        'set' => 0,
        'delete' => 0,
        'hit' => 0,
        'miss' => 0,
        'adds' => 0,
        'replaces' => 0,
        'incrs' => 0,
        'decrs' => 0
    ];
    
    /**
     * 是否启用缓存
     * @var bool
     */
    private $enabled = true;
    
    /**
     * 全局缓存组
     * @var array
     */
    private $global_groups = [];

    /**
     * 全局缓存组(使用关联数组实现快速查找)
     * @var array
     */
    private $global_groups_hash = [];
    
    /**
     * 缓存键前缀
     * @var string
     */
    private $key_prefix = '';
    
    /**
     * 构造函数
     */
    public function __construct() {
        // 检查APCu扩展是否可用
        if ( !function_exists('apcu_fetch') ) {
            $this->enabled = false;
            error_log('APCu Object Cache: APCu扩展未安装或未启用');
            return;
        }
        
        // 注册关闭时的统计信息输出
        if ( defined('WP_DEBUG') && WP_DEBUG ) {
            register_shutdown_function(array($this, 'output_stats'));
        }
        
        // 初始化默认的全局组
        $this->global_groups = ['users', 'userlogins', 'usermeta', 'user_meta', 'site-transient', 'site-options', 'site-lookup', 'blog-lookup', 'blog-details', 'rss', 'global-posts'];
        // 使用 array_flip 比 array_fill_keys 更简洁高效
        $this->global_groups_hash = array_flip($this->global_groups);
        
        // 生成缓存键前缀(强制 HTTP_HOST 前缀)
        $this->generate_key_prefix();
        
        // 添加AdminBar菜单
        add_action('admin_bar_menu', array($this, 'add_admin_bar_menu'), 100);
        
        // 处理缓存刷新请求
        add_action('admin_init', array($this, 'handle_flush_request'));
        
        // 注册AJAX处理
        add_action('wp_ajax_apcu_reset_stats', array($this, 'ajax_reset_stats'));
    }
    
    /**
     * 生成缓存键前缀(强制 HTTP_HOST 前缀)
     * 优先级:
     * 1. 始终使用 HTTP_HOST 作为基础前缀
     * 2. 如果是多站点,追加 Blog ID 做进一步隔离
     */
    private function generate_key_prefix(): void {
        // 获取 HTTP_HOST,清理非法字符并截断
        $host = $_SERVER['HTTP_HOST'] ?? 'localhost';

        // 验证Host格式,防止恶意注入
        if ( !filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) ) {
            $host = 'localhost';
        }

        $host = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $host);
        $host = substr($host, 0, 50);

        // 基础前缀 - 添加随机盐增强安全性
        $salt = defined('APCU_CACHE_SALT') ? APCU_CACHE_SALT : md5(__FILE__);
        $prefix = $host . '_' . substr($salt, 0, 8) . '_';

        // 如果是多站点环境,追加 Blog ID 做隔离
        if ( is_multisite() ) {
            $blog_id = get_current_blog_id();
            $this->key_prefix = $prefix . 'ms_' . $blog_id . '_';

            if ( WP_DEBUG ) {
                error_log('APCu Object Cache: Multisite detected, prefix: ' . $this->sanitize_for_log($this->key_prefix));
            }
            return;
        }

        // 非多站点直接使用 HTTP_HOST 前缀
        $this->key_prefix = $prefix;

        if ( WP_DEBUG ) {
            error_log('APCu Object Cache: Using HTTP_HOST based prefix: ' . $this->sanitize_for_log($this->key_prefix));
        }
    }

    /**
     * 清理日志输出,防止日志注入
     *
     * @param string $data 要记录的数据
     * @return string 清理后的数据
     */
    private function sanitize_for_log(string $data): string {
        // 移除换行符和其他可能导致日志注入的字符
        $data = str_replace(["\r", "\n"], '', $data);
        return $data;
    }
    
    /**
     * 构建完整的缓存键
     *
     * @param string $key 原始键
     * @param string $group 缓存组
     * @return string
     */
    private function build_key(string $key, string $group): string {
        // 验证并清理key参数,防止缓存键注入
        $key = $this->sanitize_key($key);
        $group = $this->sanitize_key($group);

        $prefix = $this->key_prefix;

        // 如果是全局组,不使用博客ID前缀(但仍保留 HTTP_HOST)
        // 使用 isset 替代 in_array 提升性能 O(1) vs O(n)
        if ( isset($this->global_groups_hash[$group]) ) {
            // 去掉末尾的多站点Blog ID部分(如果有)
            $prefix = preg_replace('/ms_\d+_$/', '', $prefix);
            $prefix = rtrim($prefix, '_') . '_global_';
        }

        return "{$prefix}{$group}_{$key}";
    }

    /**
     * 清理缓存键,防止注入攻击
     *
     * @param string $key 原始键
     * @return string 清理后的键
     */
    private function sanitize_key(string $key): string {
        // 限制长度和允许的字符
        $key = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $key);
        $key = substr($key, 0, 100);
        return $key;
    }
    
    /**
     * 从缓存获取数据
     *
     * @param string $key 缓存键
     * @param string $group 缓存组
     * @param bool $force 是否强制从缓存获取
     * @param bool $found 是否找到
     * @return mixed
     */
    public function get(string $key, string $group = 'default', bool $force = false, ?bool &$found = null): mixed {
        $this->stats['get']++;

        if ( !$this->enabled ) {
            $found = false;
            $this->stats['miss']++;
            return false;
        }

        $final_key = $this->build_key($key, $group);
        $value = apcu_fetch($final_key, $success);

        if ( $success ) {
            $found = true;
            $this->stats['hit']++;
            return $value instanceof \stdClass ? clone $value : $value;
        }

        $found = false;
        $this->stats['miss']++;
        return false;
    }
    
    /**
     * 设置缓存数据
     *
     * @param string $key 缓存键
     * @param mixed $data 缓存数据
     * @param string $group 缓存组
     * @param int $expire 过期时间(秒)
     * @return bool
     */
    public function set(string $key, mixed $data, string $group = 'default', int $expire = 0): bool {
        $this->stats['set']++;

        if ( !$this->enabled ) {
            return false;
        }

        $final_key = $this->build_key($key, $group);

        if ( $data instanceof \stdClass ) {
            $data = clone $data;
        }

        // 使用 isset 替代 in_array 提升性能 O(1) vs O(n)
        if ( isset($this->global_groups_hash[$group]) ) {
            $expire = 0;
        }

        return apcu_store($final_key, $data, $expire);
    }
    
    /**
     * 添加缓存数据(仅当键不存在时)
     */
    public function add(string $key, mixed $data, string $group = 'default', int $expire = 0): bool {
        if ( !$this->enabled ) {
            return false;
        }

        $final_key = $this->build_key($key, $group);

        if ( apcu_exists($final_key) ) {
            return false;
        }

        $this->stats['adds']++;
        // 处理全局组过期时间
        if ( isset($this->global_groups_hash[$group]) ) {
            $expire = 0;
        }
        // 克隆对象(与 set 方法保持一致)
        if ( $data instanceof \stdClass ) {
            $data = clone $data;
        }
        // 避免重复构建 key,直接使用已构建的 $final_key
        return apcu_store($final_key, $data, $expire);
    }
    
    /**
     * 替换缓存数据(仅当键存在时)
     */
    public function replace(string $key, mixed $data, string $group = 'default', int $expire = 0): bool {
        if ( !$this->enabled ) {
            return false;
        }

        $final_key = $this->build_key($key, $group);

        if ( !apcu_exists($final_key) ) {
            return false;
        }

        $this->stats['replaces']++;
        // 处理全局组过期时间
        if ( isset($this->global_groups_hash[$group]) ) {
            $expire = 0;
        }
        // 克隆对象(与 set 方法保持一致)
        if ( $data instanceof \stdClass ) {
            $data = clone $data;
        }
        return apcu_store($final_key, $data, $expire);
    }
    
    /**
     * 删除缓存数据
     */
    public function delete(string $key, string $group = 'default'): bool {
        $this->stats['delete']++;

        if ( !$this->enabled ) {
            return false;
        }

        return apcu_delete($this->build_key($key, $group));
    }
    
    /**
     * 递增数值
     */
    public function incr(string $key, int $offset = 1, string $group = 'default'): int|false {
        if ( !$this->enabled ) {
            return false;
        }

        $this->stats['incrs']++;
        return apcu_inc($this->build_key($key, $group), $offset);
    }
    
    /**
     * 递减数值
     */
    public function decrement(string $key, int $offset = 1, string $group = 'default'): int|false {
        if ( !$this->enabled ) {
            return false;
        }

        $this->stats['decrs']++;
        return apcu_dec($this->build_key($key, $group), $offset);
    }
    
    /**
     * 清空缓存
     */
    public function flush(): bool {
        return $this->enabled && apcu_clear_cache();
    }
    
    /**
     * 获取缓存统计信息
     */
    public function stats(): array {
        $apcu_info = apcu_cache_info(true);

        return [
            'enabled' => $this->enabled,
            'hits' => $this->stats['hit'],
            'misses' => $this->stats['miss'],
            'gets' => $this->stats['get'],
            'sets' => $this->stats['set'],
            'deletes' => $this->stats['delete'],
            'adds' => $this->stats['adds'],
            'replaces' => $this->stats['replaces'],
            'incrs' => $this->stats['incrs'],
            'decrs' => $this->stats['decrs'],
            'hit_rate' => $this->stats['get'] > 0 ? round(($this->stats['hit'] / $this->stats['get']) * 100, 2) : 0,
            'apcu_memory' => $apcu_info['memory_size'] ?? 0,
            'apcu_memory_free' => $apcu_info['mem_size'] ?? 0,
            'apcu_entries' => $apcu_info['num_entries'] ?? 0,
            'apcu_hits' => $apcu_info['num_hits'] ?? 0,
            'apcu_misses' => $apcu_info['num_misses'] ?? 0,
        ];
    }
    
    /**
     * 输出统计信息(调试用)
     */
    public function output_stats(): void {
        if ( WP_DEBUG && $this->enabled ) {
            $stats = $this->stats();
            // 清理统计数据防止日志注入
            foreach ($stats as $key => $value) {
                if (is_string($value)) {
                    $stats[$key] = $this->sanitize_for_log($value);
                }
            }
            error_log('APCu Object Cache Stats: ' . print_r($stats, true));
        }
    }
    
    /**
     * 添加全局缓存组
     */
    public function add_global_groups(string|array $groups): void {
        if ( !is_array($groups) ) {
            $groups = [$groups];
        }

        // 只处理不存在的组,避免不必要的 array_merge 和 array_unique
        foreach ( $groups as $group ) {
            // 清理group名称,防止注入
            $group = $this->sanitize_key($group);

            if ( !isset($this->global_groups_hash[$group]) ) {
                $this->global_groups[] = $group;
                $this->global_groups_hash[$group] = true;
            }
        }
        // 确保 global_groups 中的值唯一(避免重复添加)
        $this->global_groups = array_unique($this->global_groups);
    }
    
    /**
     * 获取全局缓存组
     */
    public function get_global_groups(): array {
        return $this->global_groups;
    }
    
    /**
     * AdminBar菜单
     */
    public function add_admin_bar_menu(\WP_Admin_Bar $wp_admin_bar): void {
        if ( !current_user_can('manage_options') ) {
            return;
        }

        $stats = $this->stats();

        $wp_admin_bar->add_node([
            'id'    => 'apcu-cache',
            'title' => '<span class="ab-icon dashicons dashicons-performance"></span> APCu缓存',
            'href'  => '#',
            'meta'  => ['title' => 'APCu对象缓存管理'],
        ]);

        $wp_admin_bar->add_node([
            'parent' => 'apcu-cache',
            'id'     => 'apcu-cache-stats',
            'title'  => sprintf('📊 命中率 %s%% | 命中 %d | 未命中 %d',
                esc_html($stats['hit_rate']),
                (int)$stats['hits'],
                (int)$stats['misses']
            ),
            'href'   => admin_url('?apcu_cache_action=stats'),
            'meta'   => ['title' => '查看详细统计信息'],
        ]);

        $memory_mb = round($stats['apcu_memory'] / 1024 / 1024, 2);
        $wp_admin_bar->add_node([
            'parent' => 'apcu-cache',
            'id'     => 'apcu-cache-memory',
            'title'  => sprintf('💾 内存: %s MB | 条目: %d',
                esc_html($memory_mb),
                (int)$stats['apcu_entries']
            ),
            'href'   => '#',
            'meta'   => ['title' => 'APCu内存使用情况'],
        ]);

        $wp_admin_bar->add_node([
            'parent' => 'apcu-cache',
            'id'     => 'apcu-cache-separator',
            'title'  => '',
            'href'   => '#',
        ]);

        $wp_admin_bar->add_node([
            'parent' => 'apcu-cache',
            'id'     => 'apcu-cache-flush',
            'title'  => '🔄 刷新缓存',
            'href'   => wp_nonce_url(admin_url('?apcu_cache_action=flush'), 'apcu_cache_flush'),
            'meta'   => ['title' => '清空所有APCu缓存'],
        ]);
    }
    
    /**
     * 处理缓存刷新请求
     */
    public function handle_flush_request(): void {
        if ( !isset($_GET['apcu_cache_action']) ) {
            return;
        }

        // 添加权限检查 - 只有管理员可以刷新缓存
        if ( !current_user_can('manage_options') ) {
            wp_die('权限不足', 403);
        }

        // 验证nonce
        if ( !isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'apcu_cache_flush') ) {
            wp_die('安全验证失败,请重试。');
        }

        $action = sanitize_text_field($_GET['apcu_cache_action']);

        if ( $action === 'flush' ) {
            $result = $this->flush();
            $message = $result ? '已刷新所有APCu缓存' : '缓存刷新失败';

            add_action('admin_notices', static function() use ($message) {
                $class = str_contains($message, '失败') ? 'error' : 'success';
                echo '<div class="notice notice-' . $class . ' is-dismissible"><p>' . esc_html($message) . '</p></div>';
            });

            wp_redirect(remove_query_arg(['apcu_cache_action', '_wpnonce']));
            exit;
        }
    }
    
    /**
     * AJAX重置统计
     */
    public function ajax_reset_stats() {
        check_ajax_referer('apcu_reset_stats', '_wpnonce');
        
        if ( !current_user_can('manage_options') ) {
            wp_send_json_error('权限不足');
        }
        
        $this->stats = array_fill_keys(array_keys($this->stats), 0);
        wp_send_json_success('统计已重置');
    }
}

/**
 * 初始化缓存对象
 */
function wp_cache_init(): void {
    global $wp_object_cache;

    if ( !($wp_object_cache instanceof APCu_Object_Cache) ) {
        $wp_object_cache = new APCu_Object_Cache();
    }
}

function wp_cache_get(string $key, string $group = '', bool $force = false, ?bool &$found = null): mixed {
    global $wp_object_cache;
    return $wp_object_cache->get($key, $group, $force, $found);
}

function wp_cache_set(string $key, mixed $data, string $group = '', int $expire = 0): bool {
    global $wp_object_cache;
    return $wp_object_cache->set($key, $data, $group, $expire);
}

function wp_cache_add(string $key, mixed $data, string $group = '', int $expire = 0): bool {
    global $wp_object_cache;
    return $wp_object_cache->add($key, $data, $group, $expire);
}

function wp_cache_replace(string $key, mixed $data, string $group = '', int $expire = 0): bool {
    global $wp_object_cache;
    return $wp_object_cache->replace($key, $data, $group, $expire);
}

function wp_cache_delete(string $key, string $group = ''): bool {
    global $wp_object_cache;
    return $wp_object_cache->delete($key, $group);
}

function wp_cache_incr(string $key, int $offset = 1, string $group = ''): int|false {
    global $wp_object_cache;
    return $wp_object_cache->incr($key, $offset, $group);
}

function wp_cache_decr(string $key, int $offset = 1, string $group = ''): int|false {
    global $wp_object_cache;
    return $wp_object_cache->decrement($key, $offset, $group);
}

function wp_cache_flush(): bool {
    global $wp_object_cache;
    return $wp_object_cache->flush();
}

function wp_cache_add_global_groups(string|array $groups): void {
    global $wp_object_cache;
    $wp_object_cache->add_global_groups($groups);
}

function wp_cache_add_non_persistent_groups(string|array $groups): void {
    // APCu是持久化缓存,此函数在此实现中不做任何操作
}

function wp_cache_close(): bool {
    return true;
}

// 初始化缓存
wp_cache_init();
?>

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注