ThinkPHP分页缓存需手动实现,仅可缓存COUNT(*)结果或主查询数据;分页对象含动态上下文不可序列化,直接缓存$ list危险无效;推荐按分页参数构造缓存键并监听数据变更失效缓存。

ThinkPHP 分页本身不提供内置的「分页查询缓存」开关,所谓“开启分页缓存”,本质是缓存 COUNT(*) 结果或整页数据——但必须手动控制,框架不会自动帮你缓存 paginate() 的返回值。
为什么不能直接缓存 paginate() 返回对象
分页对象(如 $list)包含动态生成的 HTML 渲染逻辑、当前请求上下文(如 request()->param())、URL 构造器等,序列化后极易失效或引发安全问题。直接 Cache::put('page_user_1', $list) 是危险且无效的操作。
- 分页对象里有闭包、资源句柄、未序列化的模板引擎实例
- 渲染时依赖当前
$_GET或input(),缓存后翻页参数会错乱 - 哪怕只缓存数据部分(
$list->items()),总数、页码、链接仍需实时计算
真正可缓存的只有 COUNT(*) 和主查询结果
缓存要落在数据库层或业务逻辑层,而非分页对象上。关键看你的瓶颈在哪:
- 如果卡在
COUNT(*):缓存总数即可,用Cache::remember('user_count_active', 3600, fn() => UserModel::where('status', 1)->count()) - 如果主查询慢(如带 JOIN、复杂 WHERE):缓存整页数据,键名必须含分页参数,例如
'user_page_'.input('page', 1).'_size_15_'.md5(http_build_query(request()->only(['keyword', 'category_id'])))' - 别缓存「第一页」就完事——用户可能从第 5 页进来的,缓存键必须能区分所有有效分页维度
简单场景:用 cache() 方法包裹 paginate 链式调用
ThinkPHP 8 支持在查询构造器上链式调用 cache(),但它只缓存「主查询结果」(即 LIMIT 后的数据),不缓存 COUNT。适合已禁用总数统计的场景:
立即学习“PHP免费学习笔记(深入)”;
$list = UserModel::where('status', 1)
->order('id desc')
->cache('user_list_simple_p'.input('page', 1), 600) // 缓存 10 分钟
->paginate(['list_rows' => 15, 'simple' => true]);
- 这个
cache()是模型/查询构造器级缓存,底层走的是think\Cache驱动 - 缓存键由你指定,推荐包含
page和筛选参数,避免不同页互相覆盖 -
simple => true必须开启,否则 COUNT 查询不走缓存,还是慢
绕过缓存陷阱的硬核建议
多数人以为“开了缓存就快了”,其实最容易踩的坑是缓存键设计和失效策略:
- 缓存键里漏掉
order字段?排序一变,缓存内容就错位 - 用
request()->param()全量拼键?token、_t 等临时参数会让缓存碎片爆炸 - 用户删了一条记录,缓存没失效?得监听模型的
deleted事件主动Cache::delete('user_list*') - 高并发下用
Cache::remember()比Cache::get() + Cache::set()更安全,避免缓存击穿
缓存不是加一行代码就能生效的事,它和你的数据变更频率、一致性要求强绑定。宁可先关掉 COUNT,再考虑缓存,别一上来就堆 cache()。



















