前阵子接手了一个有点年头的商城项目,首页和商品详情页慢得让人抓狂。每次刷新都要查数据库、拼模板、渲染,首页还好说,详情页高峰期接口响应时间经常超过一秒。更难受的是,一到搞活动,Redis装不下的热数据加上略大的MySQL慢查询,服务直接开始掉头发。
硬着头皮上缓存刻不容缓。但简单往Redis里一塞又担心缓存更新不及时,全部页面静态化又缺乏足够的灵活性。最后敲定了一套TP8自带能力结合多级缓存的方案:动态数据用标签缓存,片段用查询缓存,整体再套一层页面静态缓存,并且利用模型事件自动失效。优化后同台机器的页面平均加载时间降到50ms以下,压力测试时照样稳如老狗。这里就把搭建流程和踩过的坑完整复盘出来。
环境准备与缓存驱动配置
首先确认你的ThinkPHP版本是8.x。项目根目录下的.env文件里加入Redis配置,默认的缓存驱动也切换到Redis:
CACHE_DRIVER = redis
REDIS_HOST = 127.0.0.1
REDIS_PORT = 6379
REDIS_PASSWORD =
REDIS_SELECT = 0
接着在config/cache.php中确保stores里redis配置已关联到上述环境变量。如果还想使用文件作为二级缓存,可以保留file驱动。
简单验证一下:
use thinkfacadeCache;
Cache::set('test', 'Hello Cache', 60);
echo Cache::get('test');
成功输出即表示Redis缓存连接无碍。
第一层:模型查询缓存与标签
商品详情页通常会拉取商品基础信息、SKU列表、商品相册等。在模型中开启查询缓存,并打上标签方便批量失效:
namespace appcommonmodel;
use thinkModel;
class Goods extends Model
{
protected $cacheTag = 'goods'; // 默认标签
// 获取基础信息(自动缓存)
public function getInfoById(int $id)
{
return $this->cache(true, 3600)->find($id);
}
// 获取相册列表
public function getAlbums(int $goodsId): array
{
return $this->hasMany(GoodsAlbum::class, 'goods_id', 'id')
->where('goods_id', $goodsId)
->cache(true, 3600, 'goods_album')
->select()
->toArray();
}
}
关键在于cache(true, 3600, 'goods_album'),它会把本次查询的结果按对象缓存起来。第三个参数是标签,所有带有相同标签的缓存可以被一次性清理。当商品数据更新时,我们在控制器或事件里操作:
use thinkfacadeCache;
// 更新商品后,清除该商品相关的所有缓存
Cache::tag('goods')->clear();
Cache::tag('goods_album')->clear();
由于清除操作仅针对相关标签,不会影响其他缓存,做到了精准失效。
第二层:控制器动态缓存 + 标签
查询缓存只缓存了模型数据,拼装视图的过程依然耗费CPU。可以将整个渲染结果也缓存起来。在控制器中封装一个方法:
namespace appindexcontroller;
use appcommonmodelGoods;
use thinkfacadeCache;
use thinkfacadeView;
class Product
{
public function detail(int $id)
{
$cacheKey = 'product_detail_' . $id;
$html = Cache::get($cacheKey);
if (!$html) {
// 模型获取数据
$goodsModel = new Goods();
$info = $goodsModel->getInfoById($id);
if (!$info) {
abort(404, '商品不存在');
}
$albums = $goodsModel->getAlbums($id);
// 渲染模板
$html = View::fetch('detail', [
'goods' => $info->toArray(),
'albums' => $albums
]);
// 写入缓存,并指定标签
Cache::tag(['goods_html', 'goods_' . $id])->set($cacheKey, $html, 7200);
}
return $html;
}
}
这样每个商品的详情页HTML会被缓存两小时。当后台修改了商品信息,我们可以调用Cache::tag('goods_html')->clear();,或者精确清除单个商品缓存Cache::tag('goods_' . $id)->clear();。大部分请求直接命中缓存,无需连接数据库和渲染模板。
第三层:页面静态化 + 条件更新
如果还想进一步降低服务器压力,可以将纯HTML静态文件写入public/static目录,由Nginx直接读取。TP8可以配合ob_start和file_put_contents:
// 生成静态文件
$staticPath = app()->getRootPath() . 'public/static/goods/' . $id . '.html';
if (!is_dir(dirname($staticPath))) {
mkdir(dirname($staticPath), 0755, true);
}
file_put_contents($staticPath, $html);
// 访问时先判断静态文件是否存在
if (file_exists($staticPath)) {
return file_get_contents($staticPath);
}
可以结合TP8的事件系统,在商品更新后自动删除对应的静态文件:
namespace appcommonevent;
use thinkfacadeFilesystem;
class GoodsUpdate
{
public function handle($event)
{
$goodsId = $event->goodsId;
$staticPath = app()->getRootPath() . 'public/static/goods/' . $goodsId . '.html';
if (file_exists($staticPath)) {
unlink($staticPath);
}
// 同时清除动态缓存
Cache::tag(['goods_html', 'goods_' . $goodsId])->clear();
}
}
这样,当后台管理员编辑商品时,静态页面被删除,下一次用户访问会自动重建最新的HTML并再次缓存。
多级缓存组合投产
把上面三层串起来,请求到达时依次检查:静态文件 → Redis动态HTML缓存 → 查询缓存并拼装HTML。整个执行流如下:
public function detail(int $id)
{
// 1. 静态文件检查
$staticFile = app()->getRootPath() . 'public/static/goods/' . $id . '.html';
if (file_exists($staticFile)) {
return file_get_contents($staticFile);
}
// 2. Redis动态HTML缓存
$cacheKey = 'product_detail_' . $id;
$html = Cache::get($cacheKey);
if ($html) {
return $html;
}
// 3. 数据库查询并渲染
$goodsModel = new Goods();
$info = $goodsModel->getInfoById($id);
// ...渲染模板得到$html...
// 4. 存储到Redis,可选的静态文件写入
Cache::tag(['goods_html', 'goods_' . $id])->set($cacheKey, $html, 7200);
file_put_contents($staticFile, $html);
return $html;
}
这个流程最大化了性能,同时通过标签保证数据一致性。此外,如果在高并发下担心缓存雪崩,可以在缓存过期时间基础上增加随机偏移:
$expire = 7200 + rand(0, 600); // 7200~7800秒之间
高级技巧:利用opcache加速缓存读取
如果你的静态文件数量可控,将它们用PHP的opcache缓存入内存也是一种极速方案。可以把生成的HTML用include方式加载:
// 将HTML静态文件保存为PHP文件(带.php扩展),内容就是
$phpCacheFile = runtime_path() . 'goods/' . $id . '.php';
file_put_contents($phpCacheFile, '<?php return ' . var_export($html, true) . ';');
// 读取
$html = include $phpCacheFile;
这种方式利用了OPcache对PHP文件的缓存,读取速度甚至比Redis更快,适合热数据极其集中的场景。
缓存键命名规范与维护
项目缓存键一多,管理就容易混乱。建议遵循以下规则:
- 使用冒号分隔命名空间,如
project:module:function:id。 - 标签用复数名称,如
goods、goods_album。 - 在开发文档中罗列所有缓存键和标签,便于团队维护。
- 通过TP8的
Cache::tag()方法统一清理,禁止直接使用clear()刷全库。
总结
ThinkPHP 8本身的缓存组件设计得很到位,只需稍加组合就能构建出适合中小型项目的多级缓存迭代。从模型查询缓存到标签化动态HTML,再到可选的静态页面,每一步都能无缝衔接。在实际的商城改造中,我们几乎没修改业务逻辑,只是在模型和控制器里合理加了几行缓存代码,访问速度就提升了十倍有余。
如果你的项目也在为数据库查询和页面渲染耗时困扰,不妨从最热的接口开始,一层层加上缓存。先做模型查询缓存,再补控制器缓存,最后叠上静态文件。相信等你感受到那种“刷新页面秒开”的快感,会觉得折腾这些配置完全值得。

