从两分钟到毫秒级:一次真实看板接口性能优化实战(已上线)
作为一名全职Java开发实习生,我始终坚信:“慢接口”不是业务复杂的问题,而是代码设计与数据访问方式的失衡。 最近,我在优化一个生产环境的“排班计划看板接口”时,经历了一场从 2分钟 → 200ms 的性能跃迁。整个过程没有引入任何中间件或分布式架构,靠的是对业务逻辑的深入理解、SQL优化、批量查询思维和代码重构。
今天,我就带大家复盘这次优化全过程,分享我在熟悉与不熟悉代码区域中如何精准定位瓶颈、果断重构、实现质的飞跃的经验。
一、问题背景:一个“慢得离谱”的看板接口
我们有一个用于生产管理的“排班计划看板”,前端需要展示如下信息:
- 排班基础信息(班次、产线、计划数量等)
- 质检员姓名
- 当前工序序号
- 反馈备注与缺陷总数
- 首检/末检状态(待检/合格/不合格)
原始接口在数据量仅150条时,响应时间竟高达 120秒以上,用户反馈“每次打开都像在等一场电影加载”。这显然不可接受。
二、第一阶段:从“熟悉区”入手,先稳住基本盘
我首先从自己熟悉的模块开始优化,目标是先解决最明显的性能问题。
1. 精简返回字段,减少数据传输开销
检查接口返回的 ZhhPlanShifts
实体类,发现包含了大量前端根本不需要的字段,如 createBy
、updateTime
、remark
等。这些字段不仅增加了数据库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;
}
五、总结:性能优化的四大心法
从熟悉区入手,建立信心
先优化自己熟悉的模块,快速见效,增强继续优化的动力。以终为始:前端需要什么,就查什么
不要“为了查而查”,避免传输和计算无用数据。终结 N+1 查询
任何循环内查数据库的代码都是性能杀手,必须重构为批量查询。善用索引,尤其是覆盖索引
让查询尽可能走索引,减少磁盘IO和回表操作。
六、后续优化方向
- SQL 层聚合:将
defectCount
计算下推到 SQL 层,使用SUM()
和GROUP BY
,进一步减少 Java 层计算。 - 本地缓存:对
sys_user
、pro_route_process
等静态数据做本地缓存(如 Caffeine)。 - 异步加载:将非核心数据(如历史反馈)异步加载,提升首屏速度。
性能优化不是一蹴而就,而是一场持续的“代码瘦身”与“数据精炼”之旅。 只要你愿意深入每一行代码,敢于质疑“为什么”,就能让系统从“龟速”变为“闪电”。
我是一个热爱性能优化的Java开发者。关注我,带你从实战中提升代码质量与系统性能。