直接缓存分页总数和当前页数据比缓存整个Paginator更安全可控,需用搜索参数哈希生成唯一缓存键(如md5(http_build_query($_GET))),分别缓存total(整数)和data(数组),再通过LengthAwarePaginator手动构造分页器。

paginate() 默认查两次 SQL(COUNT(*) + 数据),高并发下容易成为瓶颈。直接缓存分页总数和当前页数据,比缓存整个 Paginator 对象更安全、更可控。
缓存键必须包含全部搜索参数的哈希值
用固定键(如 search_total 或 article_list_page_1)会导致不同条件互相覆盖。用户搜 keyword=redis&status=1&limit=20 和 keyword=mysql&status=2&limit=15 必须命中不同缓存。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 生成键名统一用
md5(http_build_query($_GET)),不拼接字符串(避免空格、编码、顺序敏感) - 加业务前缀,例如
search_total_+ 哈希、search_data_+ 哈希,方便后期redis-cli --scan --pattern "search_*"批量清理 -
Cache::get()会静默失败如果你传入数组当键——务必确保键是字符串类型
分页数据不能缓存 Paginator 对象,只缓存 total + data 数组
Paginator 是运行时对象,含数据库连接、渲染上下文、方法绑定等,序列化后反序列化会丢失关键行为(比如 $list->render() 报错或分页链接漏参)。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 缓存两样东西:
total(整数)和data(纯数组),分别用不同键,例如:search_total_abc123、search_data_abc123 - 构造分页器时,用
LengthAwarePaginator(TP6.0+ 支持)手动注入:new LengthAwarePaginator($data, $total, $perPage, $currentPage) - 如果用 TP6.0+ 的
paginate(10, false, $total),第三个参数直接传缓存读出的$total,就不用改分页器类
Redis 驱动下 TTL 与标签管理要同步处理
缓存总数和列表数据是强关联的:总数过期了,但列表数据还在,翻页可能错乱;反之,列表数据过期了但总数没清,LengthAwarePaginator 会算错总页数。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 给同一搜索请求的
total和data使用相同expire时间(比如统一设为300秒),避免时间差引发状态不一致 - 使用
Cache::tag('search')打标时,每次写入都显式调用:Cache::tag('search')->set($totalKey, $total, 300)和Cache::tag('search')->set($dataKey, $data, 300) - 删缓存时,别只
Cache::tag('search')->clear()——高并发下可能触发雪崩;应按需Cache::tag('search')->rm($totalKey)+Cache::tag('search')->rm($dataKey) - 确认你的
cache.php配置里type是redis,Apcu不支持 tag 功能
count 查询本身也要缓存,且注意游标分页的适用边界
即使你缓存了 total,第一次访问仍要查一次 COUNT(*)。这个查询本身也该进缓存,否则冷启动压力集中。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 首次无缓存时,先执行
Db::name('article')->where(...)->count(),再把结果同时写入search_total_*和search_count_*(后者专用于监控或人工校验) - 单表千万级以上、深度分页(如第 10000 页)时,
OFFSET性能已不可控,此时缓存也救不了——该切游标分页(用created_at+id做条件),而不是硬扛paginate() - Redis 内存碎片问题在高频写入短 TTL 缓存时明显,观察
INFO memory中的mem_fragmentation_ratio,超 1.5 就要考虑调整maxmemory-policy为allkeys-lru
total/data 拆分、tag 同步清理这三点,漏掉任一个都会让缓存从优化手段变成故障源头。尤其 Cache::get() 对非字符串键的静默失败,线上很难排查。



















