适合缓存树形结构的场景是后台极少改动、前端只读展示的类目树,如商品大类、地区省市区;不适合缓存用户自定义导航菜单、权限节点树等高频变动或个性化场景。

缓存树形结构前,先确认数据是否真的适合缓存
Tree 结构一旦被缓存,后续增删改节点时,整个缓存树或相关路径都得失效——不是简单 Cache::delete('tree') 就完事。ThinkPHP 默认缓存不支持按前缀批量删除(除非用 Redis 且手动操作),所以如果业务中节点变动频繁(比如后台实时拖拽排序、多人协作编辑分类),硬缓存整棵树反而导致数据不一致或缓存雪崩。
- 适合缓存:后台极少改动、前端只读展示的类目树(如商品大类、地区省市区)
- 不适合缓存:用户自定义导航菜单、权限节点树(每次登录都可能不同)
- 验证方法:查日志看
Db::table('category')->where(...)->select()在单次请求中是否被反复调用 ≥3 次,且 SQL 完全相同
用 buildTree + cache() 组合比手写递归更安全
ThinkPHP 自带的 collection()->toTree() 或第三方 buildTree() 工具函数,本质是把扁平结果集一次性组装成嵌套数组。它不触发新查询,也不依赖模型关联,正好匹配缓存场景:你只需缓存那一个扁平数组,而不是每层递归查一次数据库。
- 正确姿势:先查全部节点(
CategoryModel::select()),再cache('category_tree', $flatList, 3600),最后在业务逻辑里cache('category_tree')->toTree() - 错误姿势:在
getSubList($id)方法里直接cache('sub_'.$id, function(){...})—— 这会生成 N 个缓存 key,且父子关系断裂 - 注意
toTree()默认用pid和id字段,若你的字段叫parent_id或tree_id,得传参数指定:toTree('parent_id', 'id')
Redis 缓存比 File 缓存更适合树形结构
File 缓存读取大数组时要反序列化整个文件,而树形结构动辄几百 KB;Redis 的 GET 是内存直取,且支持设置过期时间粒度到秒,对「定时刷新树」更可控。更重要的是:你可以用 DEL category_tree* 清掉所有相关缓存(比如带版本号的 category_tree_v2)。
- 配置示例:
'type' => 'redis', 'host' => '127.0.0.1', 'port' => 6379, 'prefix' => 'tp5_' - 避免踩坑:不要在
cache()第三个参数里传0(永久缓存),ThinkPHP 会把它当成「不过期」,但 Redis 实际可能因内存淘汰策略丢数据 - 若必须用 File 缓存,请确保
runtime/cache/目录可写,且单个缓存文件不超过 2MB(PHPunserialize()对超大字符串性能陡降)
更新节点时,别只删缓存,要主动重建
很多开发者以为 cache('category_tree', null) 或 Cache::rm('category_tree') 就够了,结果下次访问时又触发全量查询+组装,造成瞬时 DB 压力。更稳的做法是在增删改操作后,立刻重新生成并写入缓存。
- 新增节点后:
$tree = CategoryModel::select(); cache('category_tree', $tree, 3600); - 删除节点前:先查出它及所有子节点 ID,再用这些 ID 批量删对应缓存(如果用了分片缓存)
- 关键点:重建动作必须和 DB 写操作放在同一个事务里(或至少保证原子性),否则会出现「DB 已改、缓存未更新」的窗口期
最麻烦的其实是「移动节点」:父级变了,整条路径的缓存都要重算。这时候建议加个版本号字段 tree_version,每次移动就 UPDATE category SET tree_version = tree_version + 1,缓存 key 改成 category_tree_v{$version},靠 key 变化自然隔离旧数据。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











