后端接口性能优化技巧

2023-09-06 0 709

上一则:IDEA 官宣崭新预设 UI,PK vs code,通心面!

后端接口性能优化技巧

产品目录

后端接口性能优化技巧

大背景

什么样难题会引发USB操控性难题

难题化解

归纳

后端接口性能优化技巧

大背景

后端接口性能优化技巧

我负责管理的控制系统在今年初就顺利完成了机能上的工程建设,接着已经开始步入到推展期。随著推展的逐渐深入细致,接到了许多赞誉的与此同时也接到了许多对操控性的聊著。

才刚接到聊著的这时候,他们的心境是这种的:后端接口性能优化技巧

当越来越多对操控性的聊著反馈到他们这里的这时候,他们意识到,USB操控性的难题的优先级必须提高了。

接着他们就跟踪了 1 周的USB操控性监控,这个这时候他们的心境是这种的:后端接口性能优化技巧有 20 多个慢USB,5 个USB响应时间超过 5s,1 个超过 10s,其余的都在 2s 以上,稳定性不足 99.8%。作为一个优秀的后端程序员,这个数据肯定是不能忍的,他们马上就步入了漫长的USB强化之路。本文就是对他们漫长工作历程的一个归纳。后端接口性能优化技巧

什么样难题会引发USB操控性难题

后端接口性能优化技巧

这个难题的答案非常多,需要根据自己的业务场景具体分析。

这里做一个不完全的归纳:

数据库慢查询

业务逻辑复杂

线程池设计不合理

锁设计不合理

机器难题(fullGC,机器重启,线程打满)

万金油化解方式

后端接口性能优化技巧

难题化解

后端接口性能优化技巧| 慢查询(基于 mysql)

①深度分页

所谓的深度分页难题,涉及到 mysql 分页的原理。通常情况下,mysql 的分页是这种写的:select name,code from student limit 100,20

含义当然就是从 student 表里查 100 到 120 这 20 条数据,mysql 会把前 120 条数据都查出来,抛弃前 100 条,返回 20 条。

当分页所以深度不大的这时候当然没难题,随著分页的深入细致,sql 可能会变成这种:select name,code fromstudentlimit 1000000,20

这个这时候,mysql 会查出来 1000020 条数据,抛弃 1000000 条,如此大的数据量,速度一定快不起来。

那如何化解呢?一般情况下,最好的方式是增加一个条件:select name,code from student where id>1000000  limit 20

这种,mysql 会走主键索引,直接连接到 1000000 处,接着查出来 20 条数据。但是这个方式需要USB的调用方配合改造,把上次查询出来的最大 id 以参数的方式传给USB提供方,会有沟通成本(调用方:老子不改!)。

②未加索引

这个是最容易化解的难题,他们可以通过:show create table

xxxx(表名)

查看某张表的索引。具体加索引的语句网上太多了,不再赘述。不过顺便提一嘴,加索引之前,需要考虑一下这个索引是不是有必要加,如果加索引的字段区分度非常低,那即使加了索引也不会生效。

另外,加索引的 alter 操作,可能引发锁表,执行 sql 的这时候一定要在低峰期(血泪史!!!!)

③索引失效

这个是慢查询最不好分析的情况,虽然 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 是索引名。

④join 过多 or 子查询过多

我把 join 过多和子查询过多放在一起说了。一般来说,不建议使用子查询,可以把子查询改成 join 来强化。与此同时,join 关联的表也不宜过多,一般来说 2-3 张表还是合适的。

具体关联几张表比较安全是需要具体难题具体分析的,如果各个表的数据量都很少,几百条几千条,那么关联的表的可以适当多一些,反之则需要少一些。

另外需要提到的是,在大多数情况下 join 是在内存里做的,如果匹配的量比较小,或者 join_buffer 设置的比较大,速度也不会很慢。

但是,当 join 的数据量比较大的这时候,mysql 会采用在硬盘上创建临时表的方式进行多张表的关联匹配,这种显然效率就极低,本来磁盘的 IO 就不快,还要关联。

一般遇到这种情况的这时候就建议从代码层面进行拆分,在业务层先查询一张表的数据,接着以关联字段作为条件查询关联表形成 map,接着在业务层进行数据的拼装。

一般来说,索引建立正确的话,会比 join 快许多,毕竟内存里拼接数据要比网络传输和硬盘 IO 快得多。

⑤in 的元素过多

这种难题,如果只看代码的话不太容易排查,最好结合监控和数据库日志一起分析。如果一个查询有 in,in 的条件加了合适的索引,这个这时候的 sql 还是比较慢就可以高度怀疑是 in 的元素过多。

一旦排查出来是这个难题,化解起来也比较容易,不过是把元素分个组,每组查一次。想再快的话,可以再引入多线程。

进一步的,如果in的元素量大到一定程度还是快不起来,这种最好还是有个限制:select id from student where id in (1,2,3 …… 1000limit 200

当然了,最好是在代码层面做个限制:

if (ids.size() > 200

) {

    throw new Exception(“单次查询数据量不能超过200”

);

}

⑥单纯的数据量过大

这种难题,单纯代码的修修补补一般就化解不了了,需要变动整个的数据存储架构。或者是对底层 mysql 分表或分库+分表;或者就是直接变更底层数据库,把 mysql 转换成专门为处理大数据设计的数据库。

这种工作是个控制系统工程,需要严密的调研、方案设计、方案评审、操控性评估、开发、测试、联调,与此同时需要设计严密的数据迁移方案、回滚方案、降级措施、故障处理预案。

除了以上团队内部的工作,还可能有跨控制系统沟通的工作,毕竟做了重大变更,下游控制系统的调用USB的方式有可能会需要变化。

出于篇幅的考虑,这个不再展开了,笔者有幸完整参与了一次亿级别数据量的数据库分表工作,对整个过程的复杂性深有体会,后续有机会也会分享出来。

| 业务逻辑复杂

①循环调用

这种情况,一般都循环调用同一段代码,每次循环的逻辑一致,前后不关联。

比如说,他们要初始化一个列表,预置 12 个月的数据给前端:List<Model> list = new

 ArrayList<>();

for(int i = 0 ; i < 12

 ; i ++) {

    Model model = calOneMonthData(i); // 计算某个月的数据,逻辑比较复杂,难以批量计算,效率也无法很高    list

.add(model);

}

这种显然每个月的数据计算相互都是独立的,他们完全可以采用多线程方式进行:// 建立一个线程池,注意要放在外面,不要每次执行代码就建立一个,具体线程池的使用就不展开了public static ExecutorService commonThreadPool = new ThreadPoolExecutor(55300

L,

        TimeUnit.SECONDS, newLinkedBlockingQueue<>(10), commonThreadFactory, new

 ThreadPoolExecutor.DiscardPolicy());

// 已经开始多线程调用List<Future<Model>> futures =new

 ArrayList<>();

for(int i = 0 ; i < 12

 ; i ++) {

    Futurefuture = commonThreadPool.submit(() -> calOneMonthData(i););

    futures.add

(future);

}

List<Model> list = new

 ArrayList<>();

try

 {

   for (int i = 0

 ; i < futures.size() ; i ++) {

      list.add(futures.get(i).get

());

   }

catch

 (Exception e) {

   LOGGER.error(“出现错误:”

, e);

}

②顺序调用

如果不是类似上面循环调用,而是一次次的顺序调用,而且调用之间没有结果上的依赖,那么也可以用多线程的方式进行,例如:

后端接口性能优化技巧

代码上看:

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 = CompletableFuture.supplyAsync(() ->

 doD(c));

CompletableFuture<E> futureE = CompletableFuture.supplyAsync(() ->

 doE(c));

CompletableFuture.allOf(futureD,futureE)//

 等d e两个任务都执行顺利完成

return

 doResult(futureD.join(),futureE.join());

这种 A B 两个逻辑可以并行执行,D E 两个逻辑可以并行执行,最大执行时间取决于哪个逻辑更慢。

| 线程池设计不合理

有的这时候,即使他们使用了线程池让任务并行处理,USB的执行效率仍然不够快,这种情况可能是怎么回事呢?

这种情况首先应该怀疑是不是线程池设计的不合理。我觉得这里有必要回顾一下线程池的三个重要参数:核心线程数、最大线程数、等待队列。

这三个参数是怎么打配合的呢?当线程池创建的这时候,如果不预热线程池,则线程池中线程为 0。当有任务提交到线程池,则已经开始创建核心线程。

后端接口性能优化技巧

当核心线程全部被占满,如果再有任务到达,则让任务步入等待队列已经开始等待。

后端接口性能优化技巧

如果队列也被占满,则已经开始创建非核心线程运行。

后端接口性能优化技巧

如果线程总数达到最大线程数,还是有任务到达,则已经开始根据线程池抛弃规则已经开始抛弃。

后端接口性能优化技巧

那么这个运行原理与USB运行时间有什么关系呢?

核心线程设置过小:核心线程设置过小则没有达到并行的效果

线程池公用,别的业务的任务执行时间太长,占用了核心线程,另一个业务的任务到达就直接步入了等待队列

任务太多,以至于占满了线程池,大量任务在队列中等待

在排查的这时候,只要找到了难题出现的原因,那么化解方式也就清楚了,无非就是调整线程池参数,按照业务拆分线程池等等。

| 锁设计不合理

锁设计不合理一般有两种:锁类型使用不合理 or 锁过粗。

锁类型使用不合理的典型场景就是读写锁。也就是说,读是可以共享的,但是读的这时候不能对共享变量写;而在写的这时候,读写都不能进行。

在可以加读写锁的这时候,如果他们加成了互斥锁,那么在读远远多于写的场景下,效率会极大降低。

锁过粗则是另一种常见的锁设计不合理的情况,如果他们把锁包裹的范围过大,则加锁时间会过长,例如:public synchronized void doSome() 

{

    File f = calData();

    uploadToS3(f);

    sendSuccessMessage();

}

这块逻辑一共处理了三部分,计算、上传结果、发送消息。显然上传结果和发送消息是完全可以不加锁的,因为这个跟共享变量根本不沾边。

因此完全可以改成:

public void doSome() 

{

    File f = null

;

    synchronized(this

) {

        f = calData();

    }

uploadToS3(f);

    sendSuccessMessage();

}

| 机器难题(fullGC,机器重启,线程打满)

造成这个难题的原因非常多,笔者就遇到了定时任务过大引发 fullGC,代码存在线程泄露引起 RSS 内存占用过高进而引发机器重启等待诸多原因。

需要结合各种监控和具体场景具体分析,进而进行大事务拆分、重新规划线程池等等工作。

| 万金油化解方式

万金油这个形容词是从他们单位某位老师那里学来的,但是笔者觉得非常贴切。这些万金油化解方式往往能化解大部分的USB缓慢的难题,而且也往往是他们化解USB效率难题的最终化解方案。

当他们实在是没有办法排查出难题,或者实在是没有强化空间的这时候,可以尝试这种万金油的方式。

①缓存

缓存是一种空间换取时间的化解方案,是在高操控性存储介质上(例如:内存、SSD 硬盘等)存储一份数据备份。

当有请求打到服务器的这时候,优先从缓存中读取数据。如果读取不到,则再从硬盘或通过网络

由于内存或 SSD 相比硬盘或网络 IO 的效率高许多,则USB响应速度会变快非常多。缓存适合于应用在数据读远远大于数据写,且数据变化不频繁的场景中。

从技术选型上看,有这些:

简单的 map

guava 等本地缓存工具包

缓存中间件:redis、tair 或 memcached

当然,memcached 现在用的很少了,因为相比于 redis 他不占优势。tair 则是阿里开发的一个分布式缓存中间件,他的优势是理论上可以在不停服的情况下,动态扩展存储容量,适用于大数据量缓存存储。

相比于单机 redis 缓存当然有优势,而他与可扩展 Redis 集群的对比则需要进一步调研。

进一步的,当前缓存的模型一般都是 key-value 模型。如何设计 key 以提高缓存的命中率是个大学问,好的 key 设计和坏的 key 设计所提升的操控性差别非常大。

而且,key 设计是没有一定之规的,需要结合具体的业务场景去分析。各个大公司分享出来的相关文章,缓存设计基本上是最大篇幅。

②回调 or 反查

这种方式往往是业务上的化解方式,在订单或者付款控制系统中应用的比较多。

举个例子:当他们付款的这时候,需要调用一个专门的付款控制系统USB,该控制系统经过一系列验证、存储工作后还要调用银行USB以执行付款。

由于付款这个动作要求十分严谨,银行侧USB执行可能比较缓慢,进而拖累整个付款USB操控性。

这个这时候他们就可以采用 fast success 的方式:当必要的校验和存储顺利完成后,立即返回 success,与此同时告诉调用方一个中间态“付款中”。

而后调用银行USB,当获得支付结果后再调用上游控制系统的回调USB返回付款的最终结果“成果”or“失败”。这种就可以异步执行付款过程,提升付款USB效率。

当然,为了防止多业务方接入的这时候回调USB不统一,可以把结果抛进 kafka,让调用方监听自己的结果。

后端接口性能优化技巧后端接口性能优化技巧

归纳

后端接口性能优化技巧

本文是笔者对工作中遇到的操控性强化难题的一个简单的归纳,可能有不完备的地方,欢迎大家加群进行技术交流。

    · END ·

后端接口性能优化技巧

正文结束

推荐阅读 ↓↓↓

1.Alibaba开源内网高并发编程手册.pdf

2.2T架构师学习资料干货分享

3.从零已经开始搭建创业公司后台技术栈

4.程序员一般可以从什么平台接私活?                                                  

后端接口性能优化技巧

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务