接口优化🚀68474ms->1329ms

简介: 接口优化🚀68474ms->1329ms

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

前言

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

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

小菜屁颠屁颠的过去后

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

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

分析与优化

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

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

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

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

image.png

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

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

     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中

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

 //存储要处理的数据
 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)

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

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

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

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

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

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

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

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

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

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

 //存储要处理的数据
 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

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

 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倍

image.png

最后

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

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

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

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

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

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

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

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

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

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

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

彩蛋

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

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

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

相关文章
|
5天前
|
存储 SQL 数据库
MSSQL存储过程的功能和用法
MSSQL存储过程的功能和用法
25 1
|
8月前
|
安全 关系型数据库 MySQL
记一次MS14-058到域控实战记录
记一次MS14-058到域控实战记录
107 0
记一次MS14-058到域控实战记录
|
5天前
|
SQL 数据库连接 数据库
MSSQL注入的入门讲解及示例
MSSQL注入的入门讲解及示例
24 0
|
5天前
|
存储 SQL 关系型数据库
探秘MSSQL存储过程:功能、用法及实战案例
探秘MSSQL存储过程:功能、用法及实战案例
|
5天前
|
SQL 缓存 关系型数据库
数据库链接池终于搞对了,这次直接从100ms优化到3ms
数据库连接池的配置是开发者们常常搞出坑的地方,在配置数据库连接池时,有几个可以说是和直觉背道而驰的原则需要明确。
|
12月前
|
SQL 消息中间件 缓存
从3s到25ms!看看京东的接口优化技巧,确实很优雅!!
从3s到25ms!看看京东的接口优化技巧,确实很优雅!!
|
SQL 消息中间件 JavaScript
从3s到25ms!看看人家的接口优化技巧,确实很优雅!! 下
从3s到25ms!看看人家的接口优化技巧,确实很优雅!! 下
|
消息中间件 缓存 JavaScript
从3s到25ms!看看人家的接口优化技巧,确实很优雅!! 上
从3s到25ms!看看人家的接口优化技巧,确实很优雅!! 上
|
SQL 存储 缓存
从11s到170ms!看看人家的接口优化技巧,那叫一个优雅!
从11s到170ms!看看人家的接口优化技巧,那叫一个优雅!
|
SQL NoSQL 关系型数据库
使用 查询分离 后 从20s优化到500ms
使用 查询分离 后 从20s优化到500ms