小菜的一次接口优化:从68474ms到1329ms
前言
突然,有人大喊一声:小菜,你过来一下
小菜被吓得抖了一抖,连忙切出开发界面,看了一眼,原来是项目经理在喊
小菜屁颠屁颠的过去后
项目经理:小菜,有空你看看后台管理里的商品信息导出Excel功能,导出数据只有几千条但是要等特别久
小菜:没问题,等我忙完手上的活就来看看怎么回事
分析与优化
小菜回到工位后,立马看了看后台管理系统的商品信息导出功能
该功能是通过导入规定格式的Excel(比如商品名称),然后导出这些商品的所有信息
小菜用对应的模板(大概数据量5千)使用此功能,大概等了1分钟多才导出结果
小菜:我可以先用arthas的trace监听这个接口,看看接口里哪些方法耗时,再具体进行分析
使用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倍
最后
接口优化的方式有很多种,在优化前我们需要进行分析哪里需要优化
在平时的开发中,也要多考虑时间、空间复杂度,并不是什么场景下都要去避免关联多张表查询
循环查数据库会造成多次网络IO,等待时间会很久,需要降低网络IO的次数,这种场景就可以联表查询
如果担心查的数据太多,联表查询性能慢,可以考虑分析执行计划增加索引,又或者分批次进行处理
其他接口优化的方式还有很多种,比如数据库优化、缓存、异步....
缓存,可以使用本地缓存、分布式缓存、多级缓存,但引入缓存又会带来一致性问题,要分析业务场景使用适合使用缓存
异步,可以使用MQ去做异步,也可以使用多线程去做异步,各有各的特点
在一些业务场景中,不要为了使用某项技术而去使用
技术是用来服务业务的,使用技术前要考虑到当前项目采用该技术是否合适,就像找伴侣一样,强扭的瓜不甜
有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~
关注菜菜,分享更多干货,公众号:菜菜的后端私房菜
彩蛋
小菜装作忧愁的来到项目经理旁边
小菜:经理,这个接口对应的实现有些复杂,我估计下周忙完手上的事情就可以优化,你先帮我提个需求吧
项目经理:ok没问题,下周忙完就尽快优化吧