3秒变30毫秒:Spring Boot 树形查询提速的关键在这~

45 阅读4分钟

沉默是金,总会发光

大家好,我是沉默

在某项目的首页分类树加载中,性能问题成了压垮系统的最后一根稻草。

  • 用户体验:分类树要加载 3~5 秒,用户频繁吐槽。

  • 系统压力:高峰期数据库连接池耗尽,系统直接崩溃。

  • 开发者噩梦:每次“优化”都只是止痛片,治标不治本。

根因是什么?一句话——递归+数据库=性能地狱
传统的递归查询触发了 N+1 查询
15000 个分类节点 = 15000 次 SQL 调用。

**
**

结果:

  • 数据库 I/O 飙升;

  • 内存压力剧增;

  • 每次打开首页都像是在加载整个世界树 。

而优化后——
响应时间从 3 秒 → 30 毫秒,提速 100 倍!

**-**01-

为什么递归会拖垮性能?

看似优雅的代码,实则暗藏杀机:

public List<Category> getCategoryTree() {    List<Category> roots = categoryMapper.getRootCategories(); // 1次查询    for (Category root : roots) {        loadChildren(root); // 每个节点都触发递归查询    }    return roots;}private void loadChildren(Category parent) {    List<Category> children = categoryMapper.getByParentId(parent.getId()); // N次查询    parent.setChildren(children);    for (Category child : children) {        loadChildren(child);    }}

问题本质:

  • 1万个节点 → 1万次 SQL 查询

  • 每次查询 2ms,总耗时 20 秒

  • 数据库连接池被打爆,GC 狂飙

性能测试结果触目惊心:

节点数量传统递归方案优化后方案提升倍数
1,000800ms15ms53倍
5,0002.8s25ms112倍
10,0005.2s30ms173倍
50,000超时45ms1000倍+

图片

- 02-

解决方案

核心思路:** **

要想快,就要“少查”。
解决方案的核心理念是:

把 N+1 次数据库查询,变成 一次查询 + 内存构建

优化四步走:

  1. 一次性批量查询:
    SELECT * FROM category WHERE status = 1

  2. 用 HashMap 建立索引:
    O(1) 时间查找父子关系。

  3. 单次遍历构建树:
    O(n) 完成整棵树构造。

  4. 结果缓存:
    内存 + Redis 多级缓存,避免重复计算。

算法级突破:

传统递归的复杂度是 O(n²):
每个节点都查数据库,查询深度越深,雪崩越快。

优化后,用一次查询 + 哈希索引替代递归:

public <T extends TreeNode<T>> List<T> buildTree(List<T> nodes, Object rootValue) {    Map<Object, T> nodeMap = nodes.stream()        .collect(Collectors.toMap(TreeNode::getId, node -> node));    List<T> roots = new ArrayList<>();    for (T node : nodes) {        Object parentId = node.getParentId();        if (Objects.equals(parentId, rootValue)) {            roots.add(node);        } else {            T parent = nodeMap.get(parentId);            if (parent != null) parent.addChild(node);        }    }    return roots;}

**
**

复杂度对比:

指标传统递归优化后提升
时间复杂度O(n²)O(n)线性级提升
数据库查询n 次1 次减少 n 倍 I/O
网络开销n 次往返1 次极限优化
缓存命中提升显著

缓存优化:

性能优化的最后一公里是“缓存重构”。
我们采用了多级缓存架构

  • L1(Caffeine 本地缓存) :毫秒级访问。
  • L2(Redis 分布式缓存) :跨节点共享。
  • Write-Invalidate 策略:更新自动失效。

同时,配合 缓存预热 + 定时刷新

@EventListener(ApplicationReadyEvent.class)public void warmUpCache() {    categoryService.getCategoryTree();}@Scheduled(fixedRate = 300000) // 每5分钟刷新一次public void refreshCache() {    categoryService.getCategoryTree();}

用户打开页面时,数据已在缓存中,首屏加载 30ms 即可返回

图片

- 03-

监控与验证

通过 Micrometer + Prometheus 监控:

public<T>List<T> monitorTreeBuild(Supplier<List<T>> builder) {    returnTimer.builder("tree.build.time")        .description("Tree building time")        .register(meterRegistry)        .recordCallable(builder::get);}

实际指标:

  • 构建时间:30ms

  • 节点数量:15,000

  • 缓存命中率:98.6%

图片

**-****04-**总结

踩坑总结与经验Tips

问题解决思路
数据更新缓存不一致延时双删 + CacheEvict
树层级过深栈溢出用迭代代替递归
Redis 序列化失败统一使用 JSON 格式
缓存预热失败增加重试与监控告警

性能优化的真相

很多人以为性能优化靠加机器,其实算法才是杠杆
从 O(n²) → O(n),从递归 → 哈希,从数据库 I/O → 内存构建,

别让递归拖垮你的树,把树交给算法来构建。

图片

**-****05-**粉丝福利

我这里创建一个程序员成长&副业交流群, 


 和一群志同道合的小伙伴,一起聚焦自身发展, 

可以聊:


技术成长与职业规划,分享路线图、面试经验和效率工具, 




探讨多种副业变现路径,从写作课程到私活接单, 




主题活动、打卡挑战和项目组队,让志同道合的伙伴互帮互助、共同进步。 




如果你对这个特别的群,感兴趣的, 
可以加一下, 微信通过后会拉你入群, 
 但是任何人在群里打任何广告,都会被我T掉。