不能直接用 Cache::get() 做热点数据常驻,因其依赖外部存储往返、存在序列化开销和锁竞争;Web FPM 下唯一可行方案是 APCu 预加载,需在启动早期手动初始化并配合定时任务自动触发。

为什么不能直接用 Cache::get() 做热点数据常驻?
因为默认的缓存驱动(如 file、redis)都是按需读取,每次请求仍要走一次 IO 或网络调用;即使命中缓存,也存在序列化/反序列化开销和锁竞争(尤其 Redis 在高并发下)。真正的“常驻内存”意味着数据在 PHP 进程生命周期内始终可直接访问,不依赖外部存储往返。
ThinkPHP 本身没有内置的“进程级全局变量缓存”,所以必须绕过框架缓存层,自己管理内存引用。
- 不要在控制器里反复调用
Cache::get('hot_tags')—— 这仍是缓存驱动调用,不是内存常驻 - 避免用
static $cache = []在类中缓存 —— FPM 模式下每个请求是独立进程,static 变量不会跨请求保留 - CLI 或 Swoole 场景下可以,但 Web FPM 下必须换思路
Web FPM 环境下唯一可行的常驻方案:apcu + 预加载钩子
APCu 是 PHP 的用户进程内共享内存扩展,支持跨请求读写(在同一个 FPM worker 内),且无网络延迟。ThinkPHP 6+ 已原生支持 apcu 驱动,但默认不启用预加载逻辑 —— 你需要手动触发一次初始化。
推荐在应用启动早期(如 app/common.php 或中间件 handle() 开头)执行预热:
// 示例:预加载热门标签(假设数据来自数据库)
if (function_exists('apcu_fetch') && !apcu_exists('hot_tags')) {
$tags = \think\facade\Db::name('tag')
->where('is_hot', 1)
->limit(100)
->column('name', 'id');
apcu_store('hot_tags', $tags, 3600); // 1小时过期,防内存泄漏
}
- 务必检查
apcu.enabled=1和apcu.shm_size(建议 ≥32M)已配置在 php.ini 中 - 不要用
Cache::store('apcu')->set()替代apcu_store()—— ThinkPHP 的apcu驱动做了额外封装,有 key 前缀和序列化,性能反而更低 - 预加载时机很关键:放在
app/init.php里可能太早(Db 尚未初始化),放在首次请求的中间件里又晚了;推荐用think\Console::init()后的事件或自定义命令触发
如何让预加载自动触发,而不是靠人工访问?
靠用户第一次访问来触发预热,会导致首屏慢、缓存击穿。正确做法是用定时任务(crontab)或部署脚本主动调用预热命令。
新建命令类 app/command/PreloadHotData.php:
php
<?php namespace app\command;
use think\command\Command;
use think\console\Input;
use think\console\Output;
class PreloadHotData extends Command
{
protected function configure()
{
$this->setName('preload:hot')->setDescription('预加载热点数据到 APCu');
}
protected function execute(Input $input, Output $output)
{
// 手动初始化 Db(绕过容器)
$db = \think\facade\Db::connect();
$hotTags = $db->name('tag')->where('is_hot', 1)->limit(50)->select()->toArray();
if (function_exists('apcu_store')) {
apcu_store('hot_tags', array_column($hotTags, 'name', 'id'), 3600);
$output->writeln('✅ hot_tags loaded to APCu');
}
}
}
- 部署时执行
php think preload:hot,确保新 worker 启动前已有数据 - FPM reload 后,所有新 worker 会各自执行一次该命令(可通过判断
apcu_exists()避免重复) - 不要在
worker_start回调里做 —— ThinkPHP FPM 不暴露该钩子,Swoole 才有
从代码里怎么安全读取 APCu 常驻数据?
直接用 apcu_fetch(),但必须处理未命中和类型异常 —— APCu 返回 false 表示未命中或存储失败,不是空数组。
// 推荐封装成工具方法
function get_hot_tags(): array
{
$data = apcu_fetch('hot_tags');
return is_array($data) ? $data : [];
}
// 使用处
$tags = get_hot_tags(); // 直接内存读取,零延迟
- 永远不要对
apcu_fetch()返回值做empty()判断 —— false、[]、0 都会被判空,应严格用is_array() - 如果业务允许稍旧数据,可设较长 TTL(如 1 小时),避免频繁重载;若要求强一致,需配合消息队列清空 APCu key(如 tag 更新后发 MQ,消费者执行
apcu_delete('hot_tags')) - APCu 不支持复杂对象自动序列化,只推荐存数组、字符串、数字;对象需手动
json_encode()存,json_decode()取
APCu 的 key 共享范围仅限于同一 FPM pool,不同 pool(如 admin / api)之间不互通;多服务器部署时,每台机器都要独立预热 —— 这不是缺陷,而是设计使然。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










