需求说明
这次接到的一个需求,需要优化一个接收包裹号的接口。产品反馈最近客户方调用接口请求经常超时,需要我们优化,减少超时情况的发生。经过我们检查发现这是个已经运行了半年的旧接口,既然是最近才反馈,大概是数据量增长后导致的慢查询,接下来查看代码后也发现的确有这个原因,但除此之外,这段代码也有些坏味道,让我们一起分析分析(想直接知道哪四步看总结哦)。
现状描述
目的
接收客户的包裹数据集合,并记录包裹状态到我们的业务数据表,以供用户在界面上查看包裹信息的同时,能够看到包裹在客户方系统的状态,能及时关注到货物运输的进度。目前代码实现流程图如下所示
流程图
通过上面的流程图,大家可能一眼就发现了其中的猫腻,让我们分析下问题在哪。
伪代码
public AjaxResult paStatus(List<PaStatusForm> paStatusList) {for (PaStatusForm form : paStatusList) {// 查询该包裹号是否属于我们系统PaInfo paInfo = paStatusMapper.selectPaInfo(form.getPaNo());if (paInfo == null) {return AjaxResult.error("包号不存在");}// 查询状态表中该包裹号是否存在,不存在则新增,存在则更新PaStatusDetail detail = paStatusMapper.selectPaStatusDetail(form.getPaNo());if (detail == null) {detail = new PaStatusDetail();detail.setId(getNextUniqueId(PaStatusDetail.class));detail.setBusinessId(paInfo.getId());detail.setPaNo(form.getPaNo());detail.setStatus(form.getStatus());paStatusMapper.insertPaStatusDetail(detail);} else {detail.setStatus(form.getStatus());paStatusMapper.updatePaStatus(detail);}}return AjaxResult.success();
}
发现问题
- 问题一:接收到的数据是一个数组/集合,程序每次处理都是逐个元素处理,每行数据都查询一次数据库;
- 问题二:查询检查,插入/更新糅合在一起,导致整个事务比较大,事务时间较长;
- 问题三:再进一步,用户经常查询的数据,对于这个包裹状态的实时性是否硬性要求呢?
优化思路
- 针对问题一,我们优化下查询语句,将集合中的元素通过修改查询语句,将
每100个
包号拼接为IN
查询,避免一个包号查询一次,减少数据库连接的次数; - 针对问题二,我们将数据的查询检查和事务处理拆分。检查和数据查询整理应该和数据库写事务进行分离,在进行写事务操作时,走批量更新的方式,接口优化后的流程如下:
通过上述优化后,接口效率已经得到了很大的提升; - 对于问题三,是否有必要呢?经过我们生产环境的“试验”(我也不想试验的,谁知道接口偶尔还是慢)来看是有的,经过日志监控,我们发现更新条件并不是主键,因此并发执行
update
时真的很慢,即使做了前两个问题的优化后,接口在大数据量的情况下响应时间依然很长。在跟产品了解了用户对于这个状态的实时性要求并不高,所以决定将接口逻辑和修改业务表的逻辑拆分
,简化接口逻辑,修改为两个流程:
伪代码
public AjaxResult paStatus(List<PaStatusForm> paStatusList) {List<TempPaStatusDetail> insertTempList = new ArrayList<>();// 每100个包号查询一次, 需要自己实现subPaNoList哦List<List<String>> subPaNoList = subPaNoList(paStatusList);List<PaInfo> existsPaInfos = new ArrayList<>();for (List<String> subPaNo : subPaNoList) {// 将存在的数据都放入集合中, 每100个包查询一次,减少查询次数,提高效率List<PaInfo> paInfos = paStatusMapper.selectPaInfos(subPaNo);if (paInfos.size() > 0) {existsPaInfos.addAll(paInfos);}}// 通过程序找到不存在的包if (existsPaInfos.size() != paStatusList.size()) {return AjaxResult.error("包号数据量不匹配,存在非法包");}// 只在插入逻辑开启事务控制AopContext.currentProxy().batchInsertPaStatusDetail(insertTempList);return AjaxResult.success();
}
@Transactional
public void batchInsertPaStatusDetail() {// 批处理代码实现...
}// 异步处理的代码逻辑也需自己实现喔。。。
通过上述优化后,接口响应时间都能保证在0.3秒内
返回。
总结
- 这次的接口性能优化主要思路为以下四步,一般的接口优化通过这几步大体都能完成优化工作了:
- 优化查询sql:提高数据校验,查询组装等逻辑的执行效率;
- 避免频繁创建数据库连接:修改数据库时考虑能否执行批量处理的操作,因为创建数据库连接比较耗时;
- 避免大事务:拆分检查组装数据和数据库事务,在修改数据库操作时才开启事务处理;
- 简化逻辑,从接口中拆分出重业务逻辑,让接口执行的事情更轻量化。
- 由于我们系统没有接入消息队列,所以图中的
待处理数据表流程
流程图在我的系统中是通过每5分钟执行一次
的定时器去完成的,如果你的系统有接入消息队列
会更好,数据处理更及时,而不用等定时器到某个时间点时才一并去处理,除了降低应用的符合外,可能还能减少数据的堆积?
大家如果有什么更好的见解也可以互相交流哈。