You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

REST API POST请求中循环内重复DB调用过多及scanRoute方法超时优化需求

优化方案:减少数据库往返次数解决超时问题

原代码的核心问题是递归过程中每个节点都触发两次独立的数据库查询(calculateDuration和findElements),当节点数量较多时,会产生大量N+1查询,导致数据库往返次数爆炸,最终引发超时。下面给出两种针对性优化方案:

方案一:批量预加载所有数据,内存中计算路径

通过一次性批量查询所有需要的数据,将递归过程中的DB调用转移到初始化阶段,后续完全在内存中完成路径计算。

步骤1:实现批量查询方法

首先需要在DAO/Repo层新增两个批量查询接口,减少单次查询的次数:

  • calculateDurationBatch(List<Integer> projectIds):批量查询多个Project的duration,返回Map<Integer, Integer>(key为Project ID,value为对应的duration)
  • findElementsBatch(List<Integer> projectIds):批量查询多个Project的后继节点,返回Map<Integer, List<Project>>(key为父Project ID,value为对应的后继列表)

步骤2:修改核心逻辑代码

private Integer getLongestDuration(List<Project> projects) {
    if (projects == null || projects.isEmpty())
        return 0;

    // 1. 收集所有相关的Project ID(根节点 + 所有层级的后继节点)
    Set<Integer> allProjectIds = new HashSet<>();
    Queue<Integer> queue = new LinkedList<>();
    // 初始化队列,加入所有根节点ID
    for (Project p : projects) {
        allProjectIds.add(p.getId());
        queue.add(p.getId());
    }
    // 批量遍历收集所有后继节点ID
    while (!queue.isEmpty()) {
        List<Integer> currentBatchIds = new ArrayList<>(queue);
        queue.clear();
        // 批量查询当前批次节点的后继
        Map<Integer, List<Project>> successorMap = findElementsBatch(currentBatchIds);
        for (List<Project> successors : successorMap.values()) {
            for (Project s : successors) {
                // 仅新增未收集过的ID
                if (allProjectIds.add(s.getId())) {
                    queue.add(s.getId());
                }
            }
        }
    }

    // 2. 批量获取所有节点的duration
    Map<Integer, Integer> durationMap = repoCall.calculateDurationBatch(new ArrayList<>(allProjectIds));

    // 3. 构建内存中的后继关系映射(ID -> 后继ID列表)
    Map<Integer, List<Integer>> successorIdMap = new HashMap<>();
    Map<Integer, List<Project>> allSuccessors = findElementsBatch(new ArrayList<>(allProjectIds));
    for (Map.Entry<Integer, List<Project>> entry : allSuccessors.entrySet()) {
        List<Integer> ids = entry.getValue().stream().map(Project::getId).collect(Collectors.toList());
        successorIdMap.put(entry.getKey(), ids);
    }

    // 4. 内存中计算最长路径时间
    int maxTotalTime = 0;
    for (Project root : projects) {
        int rootDuration = durationMap.getOrDefault(root.getId(), 0);
        int currentMax = calculateMaxPath(root.getId(), rootDuration, durationMap, successorIdMap);
        maxTotalTime = Math.max(maxTotalTime, currentMax);
    }

    return maxTotalTime;
}

// 纯内存递归计算,无DB调用
private int calculateMaxPath(int projectId, int accumulatedTime, Map<Integer, Integer> durationMap, Map<Integer, List<Integer>> successorIdMap) {
    int currentMax = accumulatedTime;
    List<Integer> successors = successorIdMap.getOrDefault(projectId, Collections.emptyList());
    for (int successorId : successors) {
        int addDuration = durationMap.getOrDefault(successorId, 0);
        int childMax = calculateMaxPath(successorId, accumulatedTime + addDuration, durationMap, successorIdMap);
        currentMax = Math.max(currentMax, childMax);
    }
    return currentMax;
}

优化效果

  • 数据库往返次数从O(N)(N为总节点数)降到O(K)(K为节点层级数,远小于N)
  • 递归过程完全在内存中执行,避免了频繁的DB上下文切换

方案二:利用数据库递归查询(适合支持CTE的数据库)

如果你的数据库支持递归CTE(如MySQL 8.0+、PostgreSQL、SQL Server),可以直接将路径计算逻辑推给数据库,仅需一次DB调用即可获取结果,性能最优。

示例SQL(PostgreSQL)

假设存在project表(存Project基本信息)、project_successor表(存父子关系,parent_id和child_id)、project_duration表(存每个Project的duration):

WITH RECURSIVE project_path AS (
    -- 初始节点:所有根Project
    SELECT 
        p.id AS project_id,
        p.id AS root_id,
        COALESCE(d.duration, 0) AS total_duration
    FROM project p
    LEFT JOIN project_duration d ON p.id = d.project_id
    WHERE p.id IN (:rootIds) -- 传入根节点ID列表
    UNION ALL
    -- 递归遍历所有后继节点
    SELECT 
        s.id AS project_id,
        pp.root_id,
        pp.total_duration + COALESCE(d.duration, 0) AS total_duration
    FROM project_path pp
    JOIN project_successor ps ON pp.project_id = ps.parent_id
    JOIN project s ON ps.child_id = s.id
    LEFT JOIN project_duration d ON s.id = d.project_id
)
SELECT MAX(total_duration) FROM project_path;

代码适配

在Repo层新增一个方法执行上述SQL,直接返回最长时长:

private Integer getLongestDurationByDbQuery(List<Project> projects) {
    if (projects == null || projects.isEmpty())
        return 0;
    List<Integer> rootIds = projects.stream().map(Project::getId).collect(Collectors.toList());
    return repoCall.calculateLongestPathBatch(rootIds);
}

优化效果

  • 仅需1次数据库调用即可获取结果,彻底消除N+1查询问题
  • 利用数据库的查询优化器高效计算路径,性能优于应用层内存计算

选择建议

  • 如果项目兼容性要求高(如数据库版本较低),优先选择方案一
  • 如果数据库支持递归CTE,且数据量较大,优先选择方案二,性能提升最明显

内容的提问来源于stack exchange,提问作者Narendra Murthy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 08:12:05