SpringBoot优化树形结构数据查询

在实际业务中,分类树(Category Tree)往往是内容管理、权限体系等系统的核心数据结构。随着业务规模不断扩张,原本毫无压力的树形查询,突然变成了系统性能的瓶颈。

1. 传统实现方式

通常是递归调用 getChildren(),每次访问数据库查询子节点。

java 复制代码
    public List<PubRegion> getRegionRange(String regionCode) {
        List<PubRegion> result = new ArrayList<>();
        getChildren(regionCode, result);
        return result;
    }

    private void getChildren(String code, List<PubRegion> result) {
        // 查询给定 code 值的区域
        Query query = new Query(Criteria.where("code").is(code));
        List<PubRegion> regions = mongoTemplate.find(query, PubRegion.class);
        result.addAll(regions);

        // 查询给定 code 值的区域的子区域
        query = new Query(Criteria.where("parent_code").is(code));
        List<PubRegion> subRegions = mongoTemplate.find(query, PubRegion.class);
        for (PubRegion subRegion : subRegions) {
            getChildren(subRegion.getCode(), result);
        }
    }

看似简洁,实则暗藏杀机。

问题分析:1 万节点 = 1 万次 SQL,单次查询平均 2ms → 总耗时 20 秒。

数据库连接池被瞬间榨干,内存中反复创建对象,GC 压力暴增。

结果就是:分类树几乎不可用。

2. 优化方案

优化思路:一次查询 + O(n) 算法

核心理念:数据库只查一次 ,获取所有节点;HashMap 建索引 ,O(1) 查找父子关系;单次遍历构建树 ,O(n) 时间复杂度;结果缓存 ,避免重复构建。

  • 通用树构建器
java 复制代码
@Component
public class TreeBuilder {

    public <T extends TreeNode<T>> List<T> buildTree(List<T> nodes, Object rootValue) {
        if (CollectionUtils.isEmpty(nodes)) {
            return new ArrayList<>();
        }

        // 1. 建立快速索引 O(n)
        Map<Object, T> nodeMap = nodes.stream()
            .collect(Collectors.toMap(TreeNode::getId, node -> node));

        List<T> rootNodes = new ArrayList<>();

        // 2. 单次遍历构建父子关系 O(n)
        for (T node : nodes) {
            Object parentId = node.getParentId();
            if (Objects.equals(parentId, rootValue)) {
                rootNodes.add(node);
            } else {
                T parent = nodeMap.get(parentId);
                if (parent != null) {
                    parent.addChild(node);
                }
            }
        }

        // 3. 可选:递归排序 O(n log n)
        sortTree(rootNodes);

        return rootNodes;
    }

    private <T extends TreeNode<T>> void sortTree(List<T> nodes) {
        if (CollectionUtils.isEmpty(nodes)) {
            return;
        }
//        nodes.sort(Comparator.comparing(TreeNode::getSortOrder,
//            Comparator.nullsLast(Comparator.naturalOrder())));
        for (T node : nodes) {
            if (node.hasChildren()) {
                sortTree(node.getChildren());
            }
        }
    }
}
  • 树节点基类
java 复制代码
public interface TreeNode<T> {
    Object getId();
    Object getParentId();
    //Integer getSortOrder();
    List<T> getChildren();
    void setChildren(List<T> children);

    default void addChild(T child) {
        if (getChildren() == null) {
            setChildren(new ArrayList<>());
        }
        getChildren().add(child);
    }

    default boolean hasChildren() {
        return getChildren() != null && !getChildren().isEmpty();
    }
}
  • 实体类
java 复制代码
@Data
@NoArgsConstructor
@SuperBuilder
@ApiModel(description = "区域表")
@Document(collection = "t_region")
public class PubRegion  implements TreeNode<PubRegion> {

    private ObjectId id;

    @ApiModelProperty("区域名称")
    private String name;

    @ApiModelProperty("区域拼音")
    private String pinyin;

    @ApiModelProperty("区域等级:省:0,市:1,区/镇:2")
    private Integer type;

    @ApiModelProperty("地区编号")
    private String code;

    @ApiModelProperty("上级地区")
    @Field("parent_code")
    private String parentCode;

    @ApiModelProperty("邮编")
    @Field("zip_code")
    private String zipCode;

    @ApiModelProperty("首字母")
    @Field("first_letter")
    private String firstLetter;

    private List<PubRegion> children;

    @Override
    public Object getId() {
        return code;
    }

    @Override
    public Object getParentId() {
        return parentCode;
    }

//涉及排序可以加上
//    @Override
//    public String getSortOrder() {
//        return firstLetter;
//    }
}
  • Controller实现
java 复制代码
@RestController
@Slf4j
@RequestMapping("/region")
public class RegionController {

    @Resource
    private RegionService regionService;

    @Resource
    private TreeBuilder treeBuilder;

    @Resource
    private MongoTemplate mongoTemplate;

    @GetMapping("/getRegionRange")
    public ResponseEntity<List<PubRegion>> getRegionRange(@RequestParam String regionCode) {
        List<PubRegion> region = regionService.findAll();
        return ResponseEntity.ok(treeBuilder.buildTree(region, regionCode));
    }
}

最终通过一次性查询 + 高效内存构建树的方式,将性能从 3 秒直接压缩到 30 毫秒,实现了 100 倍提速。

相关推荐
dd聊技术7 分钟前
Prompt 从代码常量搬进数据库:改一句话,不用再发一次版
java·后端
ly768911 分钟前
Spring Data MongoDB 批量写入实践:BulkOperations 批大小与写关注权衡
java·mongodb·spring·批量写入·bulkoperations·写关注·mongotemplate
9命怪猫14 分钟前
关于eBPF的JIT运维需要了解的
后端
TAKETODAY17 分钟前
Netty 是怎么把 HTTP 请求交给 MVC 的?一个请求的完整链路
java·netty
9命怪猫20 分钟前
运维eBPF 常用命令清单
后端
蚰蜒螟21 分钟前
从内核启动到 Java NIO:一次 socket accept 的源码串联之旅
java·python·nio
1360967572323 分钟前
服务器不能编译怎么办
后端
god_cvz29 分钟前
第三章:1、Tool Calling(工具调用)
后端·ai编程
会编程的吕洞宾36 分钟前
Spring AI 2.0 自校正结构化输出实战:让大模型稳定吐出合格 JSON
java·人工智能·后端·spring
bmxy小明同学43 分钟前
2026-10-09-token与成本
java·前端·数据库