最近优化了一段execl导出的代码,效果还不错,用这个业务为例,总结一下业务开发中常用的优化套路
寻找代码的性能瓶颈在哪里
使用profiler工具看看火焰图,具体是那些代码影响了性能.这里推荐arthas或者idea的 intelliJ profiler.更推荐后者非常好用
2023-10-17 15-43-13屏幕截图.png
函数调用栈,和对应的耗时都可以看到.
分析业务寻找优化的方法
这段代码的大致业务是,导出某医院的患者信息+医生信息+一些患者回答的问卷并计算问卷得分.通过数据发现用户的问卷比较多,问卷的题型也很多,单个用户的处理可以占据足够的cpu时间片不用担心平凡的线程上下文切换.从数据看患者的医生信息和问题,答案信息重复率很高,可以复用.
优化
- 使用多线程增加并行度
患者的而数量在3000左右,从数据量来说是一个可以一次接口调用可以处理完的范围,所以接口上不再做限制,在线程模型上我们使用生产者消费者的模型,每次从数据库中获取500个患者为一个批次做处理,直到这一批次处理完,再去获取下一批次做处理.单个线程处理的内容由上文所示.
从写法上来说一般使用两种方案:1.线程池分配线程 2.使用parallelStream().
- 缓存数据,加速查找过程
在之前的代码中每个患者的医生的都是在组装数据的时候查询,导致sql的倍数暴增,而且部分用户接诊医生相同,造成了不必要的查询浪费.所以查询所有医生暂存本地做使用.(临时变量)
Map<Long, String> doctorMap = doctorDOS.stream().collect(Collectors.toMap(DoctorDO::getId, DoctorDO::getDoctorName));
同样的患者问卷的问题和问题的答案也高度重复,只有回答的答案不同必须查询.由于导出的时候通常为医院开启例会的时候,医院的医生和主任都会使用该功能,所以这里使用(本地缓存)
这里使用gavua cache来做本地缓存
private final Cache<Long, FolRtqQuestionOptionDO> answerCache;
private final Cache<Long, FolRtqQuestionDO> questionCache;
public HjjPatientExportServiceImpl() {
this.answerCache = CacheBuilder.newBuilder()
.maximumSize(200)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
this.questionCache = CacheBuilder.newBuilder()
.maximumSize(200)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
}
- 初始化动态结构的大小防止不必要的内存copy,内核函数调用
在之前的代码中所有的ArrayList等结构均未初始化大小,所以在达到默认的容量大小后会触发扩容.在多种语言中扩容都意味着内存copy,所以能避免要尽量避免.
4.分批写入execl提升写入速度
当一个批次的500个患者处理完成后就可以直接写入execl文件,在写入的同时也在处理下一个批次的患者.至于是使用阻塞io还是异步io就交给execl框架去努力吧,也许不久的将来jdk都会把io_uring做好了.
ps:做完这些事之后这个导出已经在合格的请求时效内了.看完火焰图之后挺惊讶的,大部分耗时都是在数据的查询上,也就是说查询导致的网络io占据了大部分时间.这样看来我们写的大部分代码还是io密集型,看似做了很多计算对cpu来说却是小菜一碟.利用好缓存就能解决大部分问题.
网友评论