前言
注意看,这个男人叫小帅,他正在做一个老系统的重构,其中有一个查询接口慢到了不得不改的地步,面对完全不熟练的项目,一脸懵逼的他会采用什么样的手段来脱离困境呢,让我们一起走进科学。嘴瓢了,哈哈,天降大任于斯人也,必先送其屎山,请叫我掏粪男孩。骚话说过了,接下来说点正经的。其实我个人还是比较喜欢这样的优化任务的,平时我也会通过我的代码风格以及言传身教(夸张一下,都别打我)来影响同事们,我感觉大家的技巧是有提升的。但是呢,一直以来也没有一个具体的总结,那么今天就是圣剑解封之日,新手接口优化完全指南,大家一起来装逼吧!
接口背景
查询页面如图,总数其实不多就1W3,列表横向展示约90个字段。关于这里展示字段过多的问题,我提一嘴,这是用户强制要求的,并且该系统已上线,所以这个问题不能叫做问题。除此之外,该列表也并非普通的单表查询,存在联查和字段填充,约30个字段存在额外逻辑,其中最要命的库存字段需要实时查库,也就是说不能缓存。本项目为部门核心项目,因此查询细节不好说,大致就是会经过很多运算,最后拼成一个列表返回给前端。没错,大家熟悉的屎山就是这样,又臭又长。
接口现状
我,作为京城牢头兼优化小能手,自然是当仁不让(呜呜呜),成了被选中的人。项目作为核心项目,自然是有着核心的复杂度,至少刚看到项目代码和听人讲解了一遍后,我感觉和没听一样,淦。给了一天的时间来优化,这个感觉,对味了,仿佛回到了之前做看板项目的时候(详见设计方案-大数据量查询接口优化)。接口的现状自不必多说,代码中少的可怜的注释,仅有的注释也看不明白(刚开始不懂业务),大概五百行的核心代码和一堆到处乱跳的内部方法以及好几坨大SQL......令人绝望的现状,也没办法,拼好破碎的道心开始着手进行优化。
喔,对了,说一下之前的状况。该查询接口毕竟是高频查询接口,而且已经上线了,之前是已经优化过一轮了,据说没优化前甚至会触发网关超时。至于大半年后为啥又出现了接口慢的问题,应该是跟联表和多个数据源的数据膨胀有关,导致当时快的子查询慢慢变得性能低下了。
优化及梳理准备
一个查询接口的优化,我的思路还是比较简单,先上通用解决方案,再根据业务逻辑进行深度定制优化。熟练的打开查询页面,按下F12键,拿到查询接口入参和对应接口地址。拉下代码,问问同事主分支用的哪个,将正式数据同步到开发环境,启动项目,postman调用接口,一气呵成。
我测,15s,好家伙,怪不得要优化,我要是用户也受不了。并且前端展示字段是配置化的,也就是说列表查询接口还有前置的页面配置获取,加上前端渲染后,其实体感更慢。切到IDEA,看看代码,捋了捋,差点背过去,五百行的代码加上七八个SQL,还缺少注释......我先晕一会儿。
活了,接着奏乐接着舞。一般我将这种复杂查询类的接口,内部逻辑代码按照类型分为三种,数据源、计算、拼接。
- 数据源型,一般是通过SQL或者外部接口获取数据。
- 计算型,一般是某些字段需要经过计算得到,这部分代码也要单独标记,优化的时候拆分出来
- 拼接型,通常查询没有这一步,但是部分动态表格,前端会要求特殊的数据结构,这种情况下会有,但是一般也不需要优化。
如果不想重构代码的话,可以简单写个TODO打个醒目的标记,特别是数据源和计算类的代码,这将会是我们进行优化的重点。在梳理期间还可以做一下小优化,这些优化因为不太影响性能,所以不作为优化手段展示。
梳理代码
当然在进行这一步的时候,我会选择按照IDEA的醒目提示以及对代码的理解干掉一些无用和重复的代码。同时尽量重新定义变量名,改成自己能看懂的,而不是abc123之类的。
代码剪枝
代码剪枝的意思是如果内层函数没有别的引用,我会选择将代码合并到当前方法中,避免乱跳,看着麻烦。
合并循环
像这种不影响其他部分计算的循环,完全可以合并到其他必要的循环中,减少循环。这里只有一页查10条数据,循环10次比较少,看不出优化效果,但是这是个好习惯,不是吗?
抽取公共变量,避免重复操作
这一步就是梳理数据源的意义所在,当代码行数过多,并且有几个开发接手过的情况下难免发生。一个查询A,我在方法A中写过了,但是到了方法B,可能是新加的功能,换人了或者忘了之前写过,又重复一次查询A,这完全是没有意义的。
优化手段
所有的优化并不是无芯浮萍,针对问题去优化才是我们该做的,不要在收益不高的地方去使劲。在做完梳理准备后,将怀疑代码块圈起来打印运行时间,这种比较朴素,没办法再这样。
我是推荐大家搭配性能分析插件来定位,比如XRebel、Skywalking。内存和CPU一般不在查询接口优化的考虑范围,当然要看的话,我推荐IDEA自带的Profiler插件。在本次优化中,我是使用了IDEA插件市场里的XRebel来进行接口性能分析,具体的安装和使用可以网上搜一下,分析结果如下图。
这里是19秒,正常,因为正式服务器和数据库比我本地性能要高,自然要快一些。这都不重要,细看一下图,很清晰,甚至连可执行的SQL都放上去了。每一行从左到右依次是执行时间、方法名,详情里面是具体的可执行SQL。通过观察可以发现耗时比较长的有setDeliveryCalculationInfo中的setStock和setCalculationInfo方法,具体是因为SQL查询慢了,定位到了问题代码块那就可以着手进行优化了。以下仅通过实战的角度介绍作用够大的方法,就不多讲废话了。
缓存
缓存真的是空间换时间的首选利器,分布式缓存Redis或者本地缓存Caffeine任君选择。虽然是额外增加了中间件,但是现在项目一般都会有吧,所以在我看来这是改动收益性价比最高的一种方式。运用缓存也很简单,可以提前或者随查询将固定或者可预期改变时间的结果放入缓存,设置合适的过期时间即可。当然这么简单的手段,优化过一次的接口肯定早就用上了(淦,我就知道没有那么简单!),剩下的都是要求实时查询的SQL。
SQL优化
SQL优化这部分我已经讲过太多了,SQL语句优化可以看从零开始的SQL修炼手册-实战篇(32收藏),设计思维和硬件优化看口语化讲解数据库优化(11收藏)。但从我个人角度而言,因为我是做的供应链业务,主要是去简化ERP上的逻辑,定制化开发。受限于一些固定的表结构,导致很容易有大SQL,比如两三百行那种,各种联查子查询。你们懂的,反正在我这,这种复杂SQL,是性价比很低的优化操作,我一般选择不动,玩玩索引得了。
List转Map
经典空间换时间的优化手段,在算法中比较常用,当然实际开发中也是必要的优化手段。这个手段呢,我之前有篇文章性能优化-如何爽玩多线程来开发(72收藏)讲过,和同事也讲过,算是我比较喜欢的一种优化方式。先说说好处,最大的好处就是减少数据库查询或者外部接口调用次数,比如你用参数A+B查询数据库,循环调用10次就有10次查询数据库。但你换个角度,一次性全查出来然后转换为Map,将参数A+B当作Key从Map里取,那就少了9次数据库查询,这个性能提升是非常巨大的。
当然不可忽视的还有较大的难度,但只要做好准备,还是比较容易的,还记得前面提到的优化梳理吗?数据源类的代码拆分出来后,需要了解SQL的内容,如何修改SQL去一次性查出想要的数据是优化的第一步。举个例子,这里有个方法耗时12秒,里面每个SQL其实查询不慢,也就1秒,这种SQL优化还有意义吗?肯定没有啊,压缩成几百毫秒,积少成多也还是坏事啊。所以要改SQL,将这10次查询结果压缩成一次查询。
比较幸运的是只有一个条件,那么简单粗暴的改成IN查询即可。
同时因为获取了全量的数据,要特别注意内存占用的问题,避免发生OOM。常见优化手段有,减少数据传输,返回指定字段,并用精简后字段的包装类去接SQL结果,而不是直接拿实体类去接。之前有个OOM的问题就是几十万的数据,本来也不多,就那么几个有效字段,结果拿一个有一百多个字段的实体类去接,最后占用了1G内存。最后定位到问题,解决也简单,换个包装类只接有效字段,内存占用一下就降下来了。
List转Map这里我比较喜欢直接用JDK8的Stream流来转换,这里value注意一下,不一定给整个实体类,按需即可。比如我这里就是简单的产品名称对应数量,那么value用数量就行了,细节该注意还是注意一下。
最后肯定是要改一下代码的,按照如下图的思路去做变更即可。提一嘴,前期梳理准备很重要,不然你都不知道在哪改这些代码。
多线程
提升性能的不二法宝,上面List转Map主要针对的是能进行改造的SQL。所谓能改造的SQL是说,查出来的全量数据大小合适,并且本身压缩后的SQL也快。那么如果有大量联查,打死也压缩不了咋整呢,来人啊,上多线程。传世经典性能优化-如何爽玩多线程来开发(72收藏)还提到了另外三种法宝,分别是并行聚合处理数据、修改for循环为并行操作、修改Map遍历为并行操作。
- 并行聚合处理数据主要运用CompletableFuture.allOf()方法,将原本串行的操作改为并行,来压缩多阶段计算的总时间。
- 修改for循环为并行操作主要针对查询数据库或者调外部接口这种大量IO的场景。
- 修改Map遍历为并行操作则是为了提升Map的遍历效率,和修改for循环同理。
微醺码头
来码头整点薯条,休息一下。以上为通用查询接口的常用优化手段,差不多能覆盖八九成的开发情况了,接下来会扩展一下相关知识点。
请求合并
将多个请求合并,统一处理后返回结果,简单来看,就是将一个接口改成了逻辑上支持批量请求。一般是高并发情况下才会用到,并且要求调用方能支持返回后的统一处理结果,所以尽量能不用就不用。
实现的话,推荐用框架比如hystrix,这种会比较有保障。如果自己弄得话,可以用ConcurrentLinkedQueue作为收集请求的队列,CompletableFuture来控制请求的调用,newScheduledThreadPool来定时统一执行合并后的请求,这个我就不贴具体实现代码了,网上挺多。
集群计算代替单机
典型堆硬件产生质变的例子,比如将复杂的计算分片拆分到集群的各个机器上,最后汇总计算结果返回即可。最大化利用机器,这个分片思路其实蛮常见的,比如用elastic-job分布式任务调度这个定时框架里,就很明显的在入参处传递了分片。
好文推荐
看了不少,田螺哥的这篇实战总结!18种接口优化方案的总结,总结的比较全面吧。阿里开发者的浅谈系统性能提升的经验和方法则从更大的角度去阐述了性能优化这个大模块。后期我也会写一篇性能优化相关的文章,带有我的一些理解和实战,敬请期待嗷。
还有个事说一下,大家可能比较关心,最近如何挖掘项目中的亮点(多方向带案例)(114收藏)又火起来了,所以本文也被总结成一段亮点追加到文章中了,欢迎大家跳转观看。
写在最后
哈哈,酣畅淋漓,写完JVM之后再写我喜欢的就是爽快!本文依旧是为大家带来幸福快乐的装逼小技巧,为了完成人人装逼的伟大梦想,特意用相对口语化的表达方式(也是我最喜欢的)完成了本篇。说回正题,本篇是查询接口的性能优化,是针对单一场景的优化技巧。当然其他的地方也可以参考使用,毕竟优化的思路都是相通的。
再来说说近况,依旧是鸭梨山大┗|`O′|┛ 嗷~,哎,不过没办法,现实这个磨人的小妖精真是让人欲罢不能。下一篇不出意外会是口语化系列,讲MySQL,手里还攥着一篇重构系统的素材,如果顺利的话,下周或者下下周出吧。最后的最后,还是祝大家身体健康、工作顺利,这里是愿世界开满幸福快乐之花的Java菜恐龙,下周见啦~