小菜的一次接口优化:从68474ms到1329ms

小菜的一次接口优化:从68474ms到1329ms

前言

突然,有人大喊一声:小菜,你过来一下

小菜被吓得抖了一抖,连忙切出开发界面,看了一眼,原来是项目经理在喊

小菜屁颠屁颠的过去后

运营同学:小菜,有空你看看后台管理里的商品信息导出Excel功能,导出数据只有几千条但是要等特别久

小菜:没问题,等我忙完手上的活就来看看怎么回事

分析与优化

小菜回到工位后,立马看了看后台管理系统的商品信息导出功能

该功能是通过导入规定格式的Excel(比如商品名称),然后导出这些商品的所有信息

小菜用对应的模板(大概数据量5千)使用此功能,大概等了1分钟多才导出结果

小菜:我可以先用arthas的trace监听这个接口,看看接口里哪些方法耗时,再具体进行分析

使用arthas的trace命令监听端口后,发现总耗时70284ms,其中XXMessage耗时68s,导出Excel花费1.8s

小菜:那具体的业务处理应该在XXMessage里了,我先来看看

java 复制代码
     public List<ExportVO> xxMessage(MultipartFile file, HttpServletRequest request, HttpServletResponse response) {
         //导出结果
         List<ExportVO> exportVOS = new ArrayList<>();
         try {
             //EasyExcel 读取模板数据
             //使用AnalysisEventListener 在读取数据时加入导出结果,在读完后进行封装操作
             EasyExcel.read(file.getInputStream(), Product.class, new AnalysisEventListener<Product>() {
                 private final List<Product> list = new ArrayList<>();
 
                 //解析完一行后如何处理
                 @Override
                 public void invoke(Product p, AnalysisContext analysisContext) {
                     doLine(p);
                 }
 
                 //解析完所有数据后如何处理
                 @Override
                 public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                     doAfter();
                 }
             }).sheet().doRead();
         } catch (IOException e) {
             e.printStackTrace();
         }
         return exportVOS;
     }

小菜:使用的EasyExcel读取,那真正的处理应该在实现的AnalysisEventListener中

小菜:让我先来看看每解析一行如何处理的

java 复制代码
 //存储要处理的数据
 List<Product> products = new ArrayList<>();
 
 private void doLine(Product data) {
     try {
         //拿到所有使用ExcelProperty的字段 
         List<Field> fields = Arrays.stream(data.getClass().getDeclaredFields())
             .filter(f -> f.isAnnotationPresent(ExcelProperty.class))
             .collect(Collectors.toList());
         
         //判断字段是否为空,为空则集合添加false不为空添加true
         List<Boolean> lines = new ArrayList<>(fields.size());
         for (Field field : fields) {
             field.setAccessible(true);
             Object value = field.get(data);
             if (value == null) {
                 lines.add(Boolean.TRUE);
             } else {
                 lines.add(Boolean.FALSE);
             }
         }
         
         //ExcelProperty的所有字段不为空 就加入集合
         if(lines.stream().allMatch(Boolean.TRUE::equals)){
             products.add(data);
         }
     } catch (Exception e) {
         log.error("parse data fail: " + e.getMessage());
     }
 }

(ExcelProperty注解用于标记表格中的列)

小菜拿出做算法题分析时间复杂度的思路

小菜:这里总共有三个循环分别是:获取使用ExcelProperty的字段、判断每个字段是否为空、allMatch匹配数组中所有元素为true

小菜:那么用时间复杂度表示就是O(3N),N为数据量,而这些集合的数据量则是使用ExcelProperty的字段,好像是固定的,并不会随着Excel表格中数据量的提升而提升,那么可以把它们看成常量,那最终时间复杂度就是常量级别O(1)

小菜:但是还用了反射会有些性能开销

小菜:咦?为啥要判断实体每个字段不为空才加入要处理的集合呢?

小菜:好像直接判断该商品名不为空就可以了吧?

小菜:用反射来实现通用性,难道这段代码是前辈复制的?

于是,小菜洋洋得意的将代码改成:

java 复制代码
 //存储要处理的数据
 List<Product> products = new ArrayList<>();
 
 private void doLine(Product data) {
     if (StringUtils.isNotEmpty(data.getProductName())) {
          products.add(data);
     }
 }

为了担心自己改错,小菜还保留原始代码,方便回滚

再来看下解析完数据后的处理方法

小菜看着这一望无际一百多行没有注释、多层if嵌套的代码,整个人都呆了

大致观看了一遍后,小菜将shit mountain代码梳理成以下代码:

java 复制代码
 //存储要处理的数据
 List<Product> products = new ArrayList<>();
 
 private void doLine(Product data) {
     if (StringUtils.isNotEmpty(data.getProductName())) {
          products.add(data);
     }
 }
 
 private void doAfter() {
     //要处理的数据为空直接返回
     if (Empty.isEmpty(products)) {
         return;
     }
     
     //循环处理数据
     products.forEach(product->{
         //根据商品名查询出商品列表 IO
         List<Sku> skus = skuService.list(product.getProductName());
         //查到商品数据为空跳过
         if (Empty.isEmpty(skus)) {
             continue;
         }
         
         //查询商品具体数据 IO
         
         //查询分类、规格... IO
         
         //封装实体 添加到导出列表
     });
 }

看到这里小菜一下就明白为什么接口这么慢了

小菜:好好好,你这样写代码是吧

小菜:不考虑查数据库的网络IO是吧,肯定是不想写联表SQL,偷懒直接用MP

小菜直接用一次联表查询替代这么多的查询,为了避免数据量太大,小菜设置每次处理的最大数据量,分多次处理

java 复制代码
 private void doAfter() {
     //要处理的数据为空直接返回
     if (Empty.isEmpty(products)) {
         return;
     }
     
     int batchSize = 520;
     //将大集合拆分为多个小集合 分批次处理
     List<List<Product>> lists = CollectionUtils.split(products,batchSize);
     
     //循环处理数据
     lists.forEach(products->{
         //转换为商品名列表
         List<String> productNames = products.stream().map(Product::getProductName).collect(Collectors.toList());
         //联表查询 IO
         List<SkuDetails> skus = skuService.list(productNames);
         skus.forEach(skus->{
             //封装实体 添加到导出列表
         });
     });    
 }

小菜优化完代码后,再用arthas监听一遍,发现这次只需要3s,速度提升近23倍

最后

接口优化的方式有很多种,在优化前我们需要进行分析哪里需要优化

在平时的开发中,也要多考虑时间、空间复杂度,并不是什么场景下都要去避免关联多张表查询

循环查数据库会造成多次网络IO,等待时间会很久,需要降低网络IO的次数,这种场景就可以联表查询

如果担心查的数据太多,联表查询性能慢,可以考虑分析执行计划增加索引,又或者分批次进行处理

其他接口优化的方式还有很多种,比如数据库优化、缓存、异步....

缓存,可以使用本地缓存、分布式缓存、多级缓存,但引入缓存又会带来一致性问题,要分析业务场景使用适合使用缓存

异步,可以使用MQ去做异步,也可以使用多线程去做异步,各有各的特点

在一些业务场景中,不要为了使用某项技术而去使用

技术是用来服务业务的,使用技术前要考虑到当前项目采用该技术是否合适,就像找伴侣一样,强扭的瓜不甜

有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~

关注菜菜,分享更多干货,公众号:菜菜的后端私房菜

彩蛋

小菜装作忧愁的来到项目经理旁边

小菜:经理,这个接口对应的实现有些复杂,我估计下周忙完手上的事情就可以优化,你先帮我提个需求吧

项目经理:ok没问题,下周忙完就尽快优化吧

相关推荐
风铃儿~4 分钟前
Spring AI 入门:Java 开发者的生成式 AI 实践之路
java·人工智能·spring
斯普信专业组9 分钟前
Tomcat全方位监控实施方案指南
java·tomcat
忆雾屿20 分钟前
云原生时代 Kafka 深度实践:06原理剖析与源码解读
java·后端·云原生·kafka
武昌库里写JAVA33 分钟前
iview Switch Tabs TabPane 使用提示Maximum call stack size exceeded堆栈溢出
java·开发语言·spring boot·学习·课程设计
gaoliheng00641 分钟前
Redis看门狗机制
java·数据库·redis
我是唐青枫43 分钟前
.NET AOT 详解
java·服务器·.net
Su米苏1 小时前
Axios请求超时重发机制
java
Undoom2 小时前
🔥支付宝百宝箱新体验!途韵归旅小帮手,让高铁归途变旅行
后端
工呈士2 小时前
MobX与响应式编程实践
前端·react.js·面试
不超限2 小时前
Asp.net Core 通过依赖注入的方式获取用户
后端·asp.net