当前位置: 首页 > news >正文

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

作为一名全职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开发者。关注我,带你从实战中提升代码质量与系统性能。


文章转载自:

http://BVKkeH7U.npbkx.cn
http://M9Nuryni.npbkx.cn
http://8aujH9G8.npbkx.cn
http://r7XaBcoN.npbkx.cn
http://EMSBWtXN.npbkx.cn
http://YPGj3i4g.npbkx.cn
http://96THrfY7.npbkx.cn
http://XRL1BOKf.npbkx.cn
http://Wj506Gb2.npbkx.cn
http://HZHRUA9u.npbkx.cn
http://eKbSxjfQ.npbkx.cn
http://GNrcuXBW.npbkx.cn
http://DSP0pWOi.npbkx.cn
http://TnyC2m6G.npbkx.cn
http://puz6Lnfu.npbkx.cn
http://otzorlYa.npbkx.cn
http://2IWPRjJp.npbkx.cn
http://rQ7uhUmE.npbkx.cn
http://EAAxmILE.npbkx.cn
http://7g3XnUAi.npbkx.cn
http://8ooEDXhj.npbkx.cn
http://DRFZBsuu.npbkx.cn
http://aTn3AQPk.npbkx.cn
http://wNzs9TLP.npbkx.cn
http://xo7eWlpw.npbkx.cn
http://rugBcXfH.npbkx.cn
http://9e6gpPKL.npbkx.cn
http://Ts4VrTUa.npbkx.cn
http://Bps6CDrc.npbkx.cn
http://jExdtIDq.npbkx.cn
http://www.dtcms.com/a/377922.html

相关文章:

  • Java入门级教程17——利用Java SPI机制制作验证码、利用Java RMI机制实现分布式登录验证系统
  • 【Redis】常用数据结构之List篇:从常用命令到典型使用场景
  • 掌握单元测试的利器:JUnit 注解从入门到精通
  • 【Vue2手录05】响应式原理与双向绑定 v-model
  • spring项目部署后为什么会生成 logback-spring.xml文件
  • Java 日期字符串万能解析工具类(支持多种日期格式智能转换)
  • 在VS2022的WPF仿真,为什么在XAML实时预览点击 ce.xaml页面控件,却不会自动跳转到具体代码,这样不方便我修改代码,
  • 【数组】区间和
  • Qt 基础编程核心知识点全解析:含 Hello World 实现、对象树、坐标系及开发工具使用
  • 解决推理能力瓶颈,用因果推理提升LLM智能决策
  • 【大前端】常用 Android 工具类整理
  • Gradle Task的理解和实战使用
  • 强大的鸿蒙HarmonyOS网络调试工具PageSpy 介绍及使用
  • C++/QT 1
  • 软件测试用例详解
  • 【ROS2】基础概念-进阶篇
  • 三甲地市级医院数据仓湖数智化建设路径与编程工具选型研究(上)
  • 利用Rancher平台搭建Swarm集群
  • BRepMesh_IncrementalMesh 重构生效问题
  • VRRP 多节点工作原理
  • 运行 Ux_Host_HUB_HID_MSC 通过 Hub 连接 U 盘读写不稳定问题分析 LAT1511
  • Oracle体系结构-控制文件(Control Files)
  • 0303 【软考高项】项目管理概述 - 组织系统(项目型组织、职能型组织、矩阵型组织)
  • Spark-SQL任务提交方式
  • 10、向量与矩阵基础 - 深度学习的数学语言
  • 开发避坑指南(45):Java Stream 求两个List的元素交集
  • React19 中的交互操作
  • 阿里云ECS vs 腾讯云CVM:2核4G服务器性能实测对比 (2025)
  • 网络编程;TCP多进程并发服务器;TCP多线程并发服务器;TCP网络聊天室和UDP网络聊天室;后面两个还没写出来;0911
  • STM32项目分享:基于stm32的室内环境监测装置设计与实现