分页缓存需手动缓存稳定数据而非依赖cache()链式调用,正确做法是缓存总数与分页数据并显式管理key和失效,配合子目录分层或redis避免性能瓶颈。

分页结果缓存对非实时数据非常有效,但直接套用 cache(true, 3600) 很容易缓存错对象、漏清缓存或命中率极低。
为什么 paginate() 加 cache() 不等于“缓存了分页”
ThinkPHP 的 paginate() 返回的是一个 Paginator 对象,它内部包含两部分:数据列表($list->items())和分页元信息(总条数、当前页、渲染 HTML 等)。而 cache(true, ...) 只会缓存查询结果集(即 SQL 执行后的数组),不会缓存 Paginator 实例本身——这意味着每次请求仍要重建分页对象、重新计算总数、重复生成 HTML 渲染逻辑。
- 缓存的只是原始数据,不是分页结构;后续调用
$list->render()仍需实例化 Paginator,可能触发额外数据库查询(如 count 查询) - 如果没显式禁用 count 缓存,TP 在分页时还会单独执行一次
COUNT(*),这个查询默认不走cache()链式调用 - 缓存键由 SQL 自动生成,但分页参数(page=2、page=3)会导致不同 SQL,生成不同缓存键 → 每页都存一份,浪费空间且无法复用
正确缓存分页数据的两种实操路径
核心思路:把「可复用的、稳定的数据」提前缓存,而不是依赖链式 cache()。
-
路径一:手动缓存分页数据 + 独立构造 Paginator
先查出全部符合条件的数据(加缓存),再用collection()->forPage()切片,并传给LengthAwarePaginator(TP6)或手动 newPaginator(TP5) -
路径二:缓存「总数 + 每页数据」两个独立片段
用固定 key 缓存总数(如article_total_1),再按页码缓存数据(如article_list_p2_1),避免每页 SQL 不同导致缓存碎片化 - 两种方式都要求缓存 key 显式可控,比如含业务标识(
category_id)、状态(status=1)等,不能依赖 TP 自动生成的哈希
缓存失效必须主动管理,不能只靠过期时间
非实时数据 ≠ 永不过期。一旦后台新增/下架文章,用户翻到第 3 页看到的仍是旧数据,而第 1 页缓存可能刚刷新 —— 这种不一致比没缓存更危险。
- 写操作(
Db::name('article')->save())后,必须同步清理相关缓存:Cache::rm('article_total_1')、Cache::rm('article_list_p1_1')、Cache::rm('article_list_p2_1')… - 更稳妥的做法是打标签:
Cache::tag('article_list')->set('p1', $data, 3600),之后用Cache::tag('article_list')->clear()一键清空所有分页缓存 - 切勿在控制器里用
if (!cache(...)) { ... }包裹整个分页逻辑——这会让每个请求都尝试反序列化缓存,反而增加 CPU 开销
文件缓存分页时,目录爆炸是真实性能瓶颈
默认 file 驱动下,100 个分类 × 每类 20 页 = 2000 个缓存文件,全堆在 runtime/cache/ 下,Linux ext4 文件系统单目录超 1 万文件就会明显变慢。
- 必须开启子目录分层:
'level' => 2,让缓存分散到类似runtime/cache/a/b/article_list_p5_c7.php的路径 - 配合
'prefix' => 'art_'避免和其他模块缓存混在一起,便于人工排查或脚本清理 - Redis 驱动天然无此问题,高并发场景下优先选 Redis,哪怕只部署单机;
type => 'redis'配置后,Cache::tag()和 TTL 控制都更可靠
分页缓存最易被忽略的点:缓存的是“结果”,不是“行为”。你缓存的不是 paginate() 这个动作,而是它背后那几条 SQL 的输出。所以 key 要稳、失效要准、存储要散——三者缺一,缓存就从减压变成添堵。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











