聊聊接⼝优化的⼏个⽅法
哪些问题会引起接⼝性能问题?
这个问题的答案⾮常多,需要根据⾃⼰的业务场景具体分析。这⾥做⼀个不完全的总结:
数据库慢查询
深度分页问题
未加索引
索引失效
join过多
⼦查询过多
in中的值太多
单纯的数据量过⼤
业务逻辑复杂
循环调⽤
顺序调⽤
线程池设计不合理
锁设计不合理
机器问题(fullGC,机器重启,线程打满)
问题解决
1、慢查询(基于mysql)
1.1 深度分页
所谓的深度分页问题,涉及到mysql分页的原理。通常情况下,mysql的分页是这样写的:
select name,code from student limit 100,20
复制代码
含义当然就是从student表⾥查100到120这20条数据,mysql会把前120条数据都查出来,抛弃前100条,返回20条。当分页所以深度不⼤的时候当然没问题,随着分页的深⼊,sql可能会变成这样:
select name,code from student limit 1000000,20
复制代码
这个时候,mysql会查出来1000020条数据,抛弃1000000条,如此⼤的数据量,速度⼀定快不起来。那如何解决呢?⼀般情况下,最好的⽅式是增加⼀个条件:
select name,code from student where id>1000000 limit 20
复制代码
这样,mysql会⾛主键索引,直接连接到1000000处,然后查出来20条数据。但是这个⽅式需要接⼝的调⽤⽅配合改造,把上次查询出来的最⼤id以参数的⽅式传给接⼝提供⽅,会有沟通成本(调⽤⽅:⽼⼦不改!)。
1.2 未加索引
这个是最容易解决的问题,我们可以通过
show create table xxxx(表名)
复制代码
查看某张表的索引。具体加索引的语句⽹上太多了,不再赘述。不过顺便提⼀嘴,加索引之前,需要考虑⼀下这个索引是不是有必要加,如果加索引的字段区分度⾮常低,那即使加了索引也不会⽣效。另外,加索引的alter操作,可能引起锁表,执⾏sql的时候⼀定要在低峰期(⾎泪史)
1.3 索引失效
这个是慢查询最不好分析的情况,虽然mysql提供了explain来评估某个sql的查询性能,其中就有使⽤的索引。但是为啥索引会失效
呢?mysql却不会告诉咱,需要咱⾃⼰分析。 ⼤体上,可能引起索引失效的原因有这⼏个(可能不完全):
需要特别提出的是,关于字段区分性很差的情况,在加索引的时候就应该进⾏评估。如果区分性很差,这个索引根本就没必要加。区分性很差是什么意思呢,举⼏个例⼦,⽐如:
某个字段只可能有3个值,那这个字段的索引区分度就很低。
再⽐如,某个字段⼤量为空,只有少量有值;
再⽐如,某个字段值⾮常集中,90%都是1,剩下10%可能是2,3,4…
进⼀步的,那如果不符合上⾯所有的索引失效的情况,但是mysql还是不使⽤对应的索引,是为啥呢?这个跟mysql的sql优化有关,mysql 会在sql优化的时候⾃⼰选择合适的索引,很可能是mysql⾃⼰的选择算法算出来使⽤这个索引不会提升性能,所以就放弃了。这种情况,可以使⽤force index 关键字强制使⽤索引(建议修改前先实验⼀下,是不是真的会提升查询效率):
select name,code from student force index(XXXXXX) where name = '天才'
复制代码
其中xxxx是索引名。
1.4 join过多 or ⼦查询过多
我把join过多 和⼦查询过多放在⼀起说了。⼀般来说,不建议使⽤⼦查询,可以把⼦查询改成join来优化。同时,join关联的表也不宜过多,⼀般来说2-3张表还是合适的。具体关联⼏张表⽐较安全是需要具体问题具体分析的,如果各个表的数据量都很少,⼏百条⼏千条,那么关联的表的可以适当多⼀些,反之则需要少⼀些。
另外需要提到的是,在⼤多数情况下join是在内存⾥做的,如果匹配的量⽐较⼩,或者join_buffer设置的⽐较⼤,速度也不会很慢。但是,当join的数据量⽐较⼤的时候,mysql会采⽤在硬盘上创建临时表
的⽅式进⾏多张表的关联匹配,这种显然效率就极低,本来磁盘的IO就不快,还要关联。
⼀般遇到这种情况的时候就建议从代码层⾯进⾏拆分,在业务层先查询⼀张表的数据,然后以关联字段作为条件查询关联表形成map,然后在业务层进⾏数据的拼装。⼀般来说,索引建⽴正确的话,会⽐join快很多,毕竟内存⾥拼接数据要⽐⽹络传输和硬盘IO快得多。
1.5 in的元素过多
这种问题,如果只看代码的话不太容易排查,最好结合监控和数据库⽇志⼀起分析。如果⼀个查询有in,in的条件加了合适的索引,这个时候的sql还是⽐较慢就可以⾼度怀疑是in的元素过多。⼀旦排查出来是这个问题,解决起来也⽐较容易,不过是把元素分个组,每组查⼀次。想再快的话,可以再引⼊多线程。
进⼀步的,如果in的元素量⼤到⼀定程度还是快不起来,这种最好还是有个限制
select id from student where id in (1,2,3 ...... 1000) limit 200
复制代码
当然了,最好是在代码层⾯做个限制
if (ids.size() > 200) {
throw new Exception("单次查询数据量不能超过200");
}
复制代码
1.6 单纯的数据量过⼤
这种问题,单纯代码的修修补补⼀般就解决不了了,需要变动整个的数据存储架构。或者是对底层mysql分表或分库+分表;或者就是直接变更底层数据库,把mysql转换成专门为处理⼤数据设计的数据库。这种⼯作是个系统⼯程,需要严密的调研、⽅案设计、⽅案评审、性能评估、开发、测试、联调,同时需要设计严密的数据迁移⽅案、回滚⽅案、降级措施、故障处理预案。除了以上团队内部的⼯作,还可能有跨系统沟通的⼯作,毕竟做了重⼤变更,下游系统的调⽤接⼝的⽅式有可能会需要变化。
出于篇幅的考虑,这个不再展开了,笔者有幸完整参与了⼀次亿级别数据量的数据库分表⼯作,对整个过程的复杂性深有体会,后续有机会也会分享出来。
2、业务逻辑复杂
2.1 循环调⽤
这种情况,⼀般都循环调⽤同⼀段代码,每次循环的逻辑⼀致,前后不关联。⽐如说,我们要初始化⼀个列表,预置12个⽉的数据给前端:
List<Model> list = new ArrayList<>();
for(int i = 0 ; i < 12 ; i ++) {
Model model = calOneMonthData(i); // 计算某个⽉的数据,逻辑⽐较复杂,难以批量计算,效率也⽆法很⾼
list.add(model);
}
复制代码
这种显然每个⽉的数据计算相互都是独⽴的,我们完全可以采⽤多线程⽅式进⾏:
// 建⽴⼀个线程池,注意要放在外⾯,不要每次执⾏代码就建⽴⼀个,具体线程池的使⽤就不展开了p
ublic static ExecutorService commonThreadPool = new ThreadPoolExecutor(5, 5, 300L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), commonThreadFactory, new ThreadPoolExecutor.DiscardPol icy());// 开始多线程调⽤List<Future<Model>> futures = new ArrayList<>();for(int i = 0 ; i < 12 ; i ++) { Future<Model> future = commonThreadPool.submit(( ) -> calOneMonthData(i);); futures.add(future);}// 获取结果List<Model> list = new ArrayList<>();try { for (int i = 0 ; i < futures.size() ; i ++) { list.add((i).get()); }} catch (Exception e) { ("出现错误:", e);}复制代码
2.2 顺序调⽤
如果不是类似上⾯循环调⽤,⽽是⼀次次的顺序调⽤,⽽且调⽤之间没有结果上的依赖,那么也可以⽤多线程的⽅式进⾏,例如:
代码上看:
A a = doA();
B b = doB();
C c = doC(a, b);
D d = doD(c);
E e = doE(c);return doResult(d, e);复制代码
那么可⽤CompletableFuture解决
CompletableFuture<A> futureA = CompletableFuture.supplyAsync(() -> doA());CompletableFuture<B> futureB = CompletableFuture.supplyAsync(() -> doB ());CompletableFuture.allOf(futureA,futureB) // 等a b 两个任务都执⾏完成C c = doC(futureA.join(), futureB.join());CompletableFuture<D> futureD = Complet ableFuture.supplyAsync(() -> doD(c));CompletableFuture<E> futureE = CompletableFuture.supplyAsync(() -> doE(c));CompletableFuture.allOf(futureD,futu reE) // 等d e两个任务都执⾏完成return doResult(futureD.join(),futureE.join());复制代码
这样A B 两个逻辑可以并⾏执⾏,D E两个逻辑可以并⾏执⾏,最⼤执⾏时间取决于哪个逻辑更慢。
3、线程池设计不合理
有的时候,即使我们使⽤了线程池让任务并⾏处理,接⼝的执⾏效率仍然不够快,这种情况可能是怎么回事呢?
这种情况⾸先应该怀疑是不是线程池设计的不合理。我觉得这⾥有必要回顾⼀下线程池的三个重要参数:核⼼线程数、最⼤线程数、等待队列。这三个参数是怎么打配合的呢?当线程池创建的时候,如果不预热线程池,则线程池中线程为0。当有任务提交到线程池,则开始创建核⼼线程。
当核⼼线程全部被占满,如果再有任务到达,则让任务进⼊等待队列开始等待。
如果队列也被占满,则开始创建⾮核⼼线程运⾏。
如果线程总数达到最⼤线程数,还是有任务到达,则开始根据线程池抛弃规则开始抛弃。
那么这个运⾏原理与接⼝运⾏时间有什么关系呢?
核⼼线程设置过⼩:核⼼线程设置过⼩则没有达到并⾏的效果
线程池公⽤,别的业务的任务执⾏时间太长,占⽤了核⼼线程,另⼀个业务的任务到达就直接进⼊了等待队列
任务太多,以⾄于占满了线程池,⼤量任务在队列中等待
在排查的时候,只要到了问题出现的原因,那么解决⽅式也就清楚了,⽆⾮就是调整线程池参数,按照业务拆分线程池等等。
4、锁设计不合理
锁设计不合理⼀般有两种:锁类型使⽤不合理 or 锁过粗。
锁类型使⽤不合理的典型场景就是读写锁。也就是说,读是可以共享的,但是读的时候不能对共享变量写;⽽在写的时候,读写都不能进⾏。在可以加读写锁的时候,如果我们加成了互斥锁,那么在读远远多于写的场景下,效率会极⼤降低。
锁过粗则是另⼀种常见的锁设计不合理的情况,如果我们把锁包裹的范围过⼤,则加锁时间会过长,例如:
public synchronized void doSome() {
File f = calData();
uploadToS3(f);
sendSuccessMessage();
}
复制代码
这块逻辑⼀共处理了三部分,计算、上传结果、发送消息。显然上传结果和发送消息是完全可以不加锁的,因为这个跟共享变量根本不沾边。因此完全可以改成:
sql优化的几种方式public void doSome() {
File f = null;
synchronized(this) {
f = calData();
}
uploadToS3(f);
sendSuccessMessage();
}
复制代码
5、机器问题(fullGC,机器重启,线程打满)
造成这个问题的原因⾮常多,笔者就遇到了定时任务过⼤引起fullGC,代码存在线程泄露引起RSS内存占⽤过⾼进⽽引起机器重启等待诸多原因。需要结合各种监控和具体场景具体分析,进⽽进⾏⼤事务拆分、重新规划线程池等等⼯作
6、万⾦油解决⽅式
万⾦油这个形容词是从我们单位某位⽼师那⾥学来的,但是笔者觉得⾮常贴切。这些万⾦油解决⽅式往往能解决⼤部分的接⼝缓慢的问题,⽽且也往往是我们解决接⼝效率问题的最终解决⽅案。当我们实在是没有办法排查出问题,或者实在是没有优化空间的时候,可以尝试这种万⾦油的⽅式。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论