从两分钟到毫秒级:一次真实看板接口性能优化实战(已上线)

发布于:2025-09-13 ⋅ 阅读:(16) ⋅ 点赞:(0)

作为一名全职Java开发实习生,我始终坚信:“慢接口”不是业务复杂的问题,而是代码设计与数据访问方式的失衡。 最近,我在优化一个生产环境的“排班计划看板接口”时,经历了一场从 2分钟 → 200ms 的性能跃迁。整个过程没有引入任何中间件或分布式架构,靠的是对业务逻辑的深入理解、SQL优化、批量查询思维和代码重构。

今天,我就带大家复盘这次优化全过程,分享我在熟悉与不熟悉代码区域中如何精准定位瓶颈、果断重构、实现质的飞跃的经验。


一、问题背景:一个“慢得离谱”的看板接口

我们有一个用于生产管理的“排班计划看板”,前端需要展示如下信息:

  • 排班基础信息(班次、产线、计划数量等)
  • 质检员姓名
  • 当前工序序号
  • 反馈备注与缺陷总数
  • 首检/末检状态(待检/合格/不合格)

原始接口在数据量仅150条时,响应时间竟高达 120秒以上,用户反馈“每次打开都像在等一场电影加载”。这显然不可接受。


二、第一阶段:从“熟悉区”入手,先稳住基本盘

我首先从自己熟悉的模块开始优化,目标是先解决最明显的性能问题

1. 精简返回字段,减少数据传输开销

检查接口返回的 ZhhPlanShifts 实体类,发现包含了大量前端根本不需要的字段,如 createByupdateTimeremark 等。这些字段不仅增加了数据库IO,还增大了网络传输体积。

优化措施:

  • 在 resultMap 中只 SELECT 前端需要的字段。
  • 使用 覆盖索引(Covering Index),让查询完全走索引,避免回表。
-- 为 zhh_plan_shifts 表添加覆盖索引
CREATE INDEX idx_covering ON zhh_plan_shifts (create_time, qc_id, route_id, line_name, line_id);

这样,数据库可以直接从索引中获取所有数据,无需访问主表。

2. 批量查询替代逐条查询(N+1问题终结)

原始代码中存在典型的 N+1 查询问题

// 问题代码:每条记录都查一次数据库
for (Long planShiftId : planShiftIds) {
    List<ProFeedback> feedbacks = proFeedbackMapper.selectProFeedbackByPlanShiftsId(planShiftId);
    feedbackMap.put(planShiftId, feedbacks);
}

150条记录 → 150次数据库查询,网络延迟叠加,性能雪崩。

优化措施:

  • 新增批量查询方法:
List<ProFeedback> selectProFeedbackByPlanShiftsIds(@Param("planShiftsIds") List<Long> planShiftsIds);
  • 一次查询获取所有反馈数据,再用 Java Stream 分组:
List<ProFeedback> allFeedbacks = proFeedbackMapper.selectProFeedbackByPlanShiftsIds(planShiftIds);
Map<Long, List<ProFeedback>> feedbackMap = allFeedbacks.stream()
    .collect(Collectors.groupingBy(ProFeedback::getPlanShiftsId));

效果:数据库查询次数从 150 次 → 1 次,性能提升立竿见影。


三、第二阶段:深入“陌生代码区”,重构逻辑,精准减负

当熟悉的部分优化完毕后,我发现接口仍耗时约 30 秒。这时,我决定深入之前“不敢动”的代码区域——那些由前同事编写、逻辑复杂、注释稀少的模块。

1. 逆向分析:从“前端需要什么”反推代码逻辑

我没有直接阅读代码,而是采取了更高效的方式:

  • 打开浏览器开发者工具,查看前端实际渲染了哪些字段。
  • 发现:qcName(质检员姓名)、serialNumber(工序序号)虽然在实体类中,但前端并未使用

但代码中却为此执行了两次额外查询:

// 查询质检员姓名(前端不用!)
List<SysUser> users = userMapper.selectUsersByIds(qcIds);

// 逐条查询工序序号(性能极差)
Integer serialNumber = proRouteProcessMapper.selectByOrderNum(ps.getRouteId(), ps.getLineName());

果断决策:删除这两段冗余代码!

📌 经验分享:不要被“代码存在即合理”束缚。如果字段前端不用,就大胆移除,避免“为了显示而查询”的陷阱。

2. 重构 IPQC 查询:批量参数化查询

原代码对 qc_ipqc 表的查询虽已批量,但方式不够优雅:

List<Map<String, Object>> queryParams = new ArrayList<>();
for (ZhhPlanShifts item : zhhPlanShiftsList) {
    Map<String, Object> param = new HashMap<>();
    param.put("planShiftsId", item.getId());
    param.put("lineId", item.getLineId());
    queryParams.add(param);
}

优化建议:改用 IN 条件或更高效的批量查询方式(如 WHERE (plan_shifts_id, line_id) IN ((1,101), (2,102))),但受限于 MyBatis 支持,当前方式已可接受。


四、代码对比:优化前 vs 优化后

优化前(耗时 > 120s)

@Override
public List<ZhhPlanShifts> selectZhhPlanShiftsListToKanban(ZhhPlanShifts zhhPlanShifts) {
    List<ZhhPlanShifts> zhhPlanShiftsList = zhhPlanShiftsMapper.selectZhhPlanShiftsList(zhhPlanShifts);

    // ❌ 多余的质检员查询
    Set<Long> qcIds = zhhPlanShiftsList.stream().map(ZhhPlanShifts::getQcId).filter(Objects::nonNull).collect(Collectors.toSet());
    Map<Long, String> userNickNameMap = new HashMap<>();
    if (!qcIds.isEmpty()) {
        List<SysUser> users = userMapper.selectUsersByIds(new ArrayList<>(qcIds));
        userNickNameMap = users.stream().collect(Collectors.toMap(SysUser::getUserId, SysUser::getNickName));
    }

    // ❌ 逐条查询工序序号(N+1)
    Map<String, Integer> serialNumberMap = new HashMap<>();
    for (ZhhPlanShifts ps : zhhPlanShiftsList) {
        Integer serialNumber = proRouteProcessMapper.selectByOrderNum(ps.getRouteId(), ps.getLineName());
        serialNumberMap.put(ps.getRouteId() + "_" + ps.getLineName(), serialNumber);
    }

    // ❌ 逐条查询反馈数据(N+1)
    for (Long planShiftId : planShiftIds) {
        List<ProFeedback> feedbacks = proFeedbackMapper.selectProFeedbackByPlanShiftsId(planShiftId);
    }

    // ... 其他处理
}

优化后(耗时 ≈ 200ms)

@Override
public List<ZhhPlanShifts> selectZhhPlanShiftsListToKanban(ZhhPlanShifts zhhPlanShifts) {
    // ✅ 覆盖索引 + 精简字段
    List<ZhhPlanShifts> zhhPlanShiftsList = zhhPlanShiftsMapper.selectZhhPlanShiftsList(zhhPlanShifts);
    if (zhhPlanShiftsList.isEmpty()) return zhhPlanShiftsList;

    // ✅ 批量查询反馈数据
    Set<Long> planShiftIds = zhhPlanShiftsList.stream().map(ZhhPlanShifts::getId).filter(Objects::nonNull).collect(Collectors.toSet());
    Map<Long, List<ProFeedback>> feedbackMap = new HashMap<>();
    if (!planShiftIds.isEmpty()) {
        List<ProFeedback> allFeedbacks = proFeedbackMapper.selectProFeedbackByPlanShiftsIds(planShiftIds);
        feedbackMap = allFeedbacks.stream().collect(Collectors.groupingBy(ProFeedback::getPlanShiftsId));
    }

    // ✅ 单次遍历填充缺陷数
    for (ZhhPlanShifts planShifts : zhhPlanShiftsList) {
        List<ProFeedback> proFeedbacks = feedbackMap.getOrDefault(planShifts.getId(), Collections.emptyList());
        long totalDefectCount = proFeedbacks.stream().mapToLong(pf -> 
            (pf.getQuantityUnquanlified() != null ? pf.getQuantityUnquanlified() : 0) +
            (pf.getAttr3() != null ? pf.getAttr3() : 0) +
            (pf.getAttr4() != null ? pf.getAttr4() : 0) +
            (pf.getQuantityTestFailed() != null ? pf.getQuantityTestFailed() : 0)
        ).sum();
        planShifts.setDefectCount(totalDefectCount);
    }

    // ✅ 批量查询 IPQC 状态
    List<Map<String, Object>> queryParams = buildQueryParams(zhhPlanShiftsList);
    List<QcIpqc> allQcIpqcs = qcIpqcMapper.selectQcIpqcListByBatch(queryParams);
    Map<Long, List<QcIpqc>> qcIpqcMap = allQcIpqcs.stream().collect(Collectors.groupingBy(QcIpqc::getPlanShiftsId));

    // ✅ 填充首检/末检状态
    for (ZhhPlanShifts item : zhhPlanShiftsList) {
        List<QcIpqc> qcIpqcs = qcIpqcMap.getOrDefault(item.getId(), Collections.emptyList());
        item.setFirstCheckStatus("未检查");
        item.setFinalCheckStatus("未检查");
        for (QcIpqc ipqc : qcIpqcs) {
            if ("FIRST".equals(ipqc.getIpqcType())) {
                item.setFirstCheckStatus(convertStatus(ipqc.getStatus()));
            } else if ("FINAL".equals(ipqc.getIpqcType())) {
                item.setFinalCheckStatus(convertStatus(ipqc.getStatus()));
            }
        }
    }

    return zhhPlanShiftsList;
}

五、总结:性能优化的四大心法

  1. 从熟悉区入手,建立信心
    先优化自己熟悉的模块,快速见效,增强继续优化的动力。

  2. 以终为始:前端需要什么,就查什么
    不要“为了查而查”,避免传输和计算无用数据。

  3. 终结 N+1 查询
    任何循环内查数据库的代码都是性能杀手,必须重构为批量查询。

  4. 善用索引,尤其是覆盖索引
    让查询尽可能走索引,减少磁盘IO和回表操作。


六、后续优化方向

  • SQL 层聚合:将 defectCount 计算下推到 SQL 层,使用 SUM() 和 GROUP BY,进一步减少 Java 层计算。
  • 本地缓存:对 sys_userpro_route_process 等静态数据做本地缓存(如 Caffeine)。
  • 异步加载:将非核心数据(如历史反馈)异步加载,提升首屏速度。

性能优化不是一蹴而就,而是一场持续的“代码瘦身”与“数据精炼”之旅。 只要你愿意深入每一行代码,敢于质疑“为什么”,就能让系统从“龟速”变为“闪电”。

我是一个热爱性能优化的Java开发者。关注我,带你从实战中提升代码质量与系统性能。