业务代码优化的一些固定套路

最近优化了一段execl导出的代码,效果还不错,用这个业务为例,总结一下业务开发中常用的优化套路

寻找代码的性能瓶颈在哪里

使用profiler工具看看火焰图,具体是那些代码影响了性能.这里推荐arthas或者idea的 intelliJ profiler.更推荐后者非常好用


2023-10-17 15-43-13屏幕截图.png

函数调用栈,和对应的耗时都可以看到.

分析业务寻找优化的方法

这段代码的大致业务是,导出某医院的患者信息+医生信息+一些患者回答的问卷并计算问卷得分.通过数据发现用户的问卷比较多,问卷的题型也很多,单个用户的处理可以占据足够的cpu时间片不用担心平凡的线程上下文切换.从数据看患者的医生信息和问题,答案信息重复率很高,可以复用.

优化

  1. 使用多线程增加并行度
    患者的而数量在3000左右,从数据量来说是一个可以一次接口调用可以处理完的范围,所以接口上不再做限制,在线程模型上我们使用生产者消费者的模型,每次从数据库中获取500个患者为一个批次做处理,直到这一批次处理完,再去获取下一批次做处理.单个线程处理的内容由上文所示.

从写法上来说一般使用两种方案:1.线程池分配线程 2.使用parallelStream().

  1. 缓存数据,加速查找过程
    在之前的代码中每个患者的医生的都是在组装数据的时候查询,导致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();
    }
  1. 初始化动态结构的大小防止不必要的内存copy,内核函数调用

在之前的代码中所有的ArrayList等结构均未初始化大小,所以在达到默认的容量大小后会触发扩容.在多种语言中扩容都意味着内存copy,所以能避免要尽量避免.

4.分批写入execl提升写入速度
当一个批次的500个患者处理完成后就可以直接写入execl文件,在写入的同时也在处理下一个批次的患者.至于是使用阻塞io还是异步io就交给execl框架去努力吧,也许不久的将来jdk都会把io_uring做好了.

ps:做完这些事之后这个导出已经在合格的请求时效内了.看完火焰图之后挺惊讶的,大部分耗时都是在数据的查询上,也就是说查询导致的网络io占据了大部分时间.这样看来我们写的大部分代码还是io密集型,看似做了很多计算对cpu来说却是小菜一碟.利用好缓存就能解决大部分问题.

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容