接口性能调优

1、哪些问题会引起接口性能问题

1、数据库慢查询
2、业务逻辑复杂
3、线程池设计不合理
4、锁设计不合理

2、问题解决

1、慢查询(基于mysql)

① 深度分页
所谓的深度分页是查询开始页码很大,这涉及到mysql的分页原理(第一个参数是页码,第二个参数是每页显示条数),通常情况下,mysql的分页是这样的:

select name,age from person limit 100,20

这条sql语句时从person表中查100到120这20条数据,mysql会把前120条数据都查询出来,再抛弃前100条,返回20条。

当分页所以深度不大的时候当然没问题,随着分页的深入,sql 可能会变成这样:

select name,age from person limit 1000000,20

这个时候,mysql 会查出来 1000020 条数据,抛弃 前面1000000 条,如此大的数据量,速度一定快不起来。
那如何解决呢?一般情况下,最好的方式是增加一个条件:

select name,code from student where id>1000000  limit 20

这种优化的方案有一个问题,就是id不连续是,比如1,1000000中间的数据有几条数据删除了,那么这种方案查询出来的数据就和原sql语句查询出来的不同了。所有这种方案是使用在id主键是连续的,并且中间没有数据删除的情况。否则慎用。

②未加索引
这个是最容易解决的问题
加索引的语句不在赘述

③索引失效
这个是慢查询最不好分析的情况,虽然 mysql 提供了 explain 来评估某个 sql 的查询性能,其中就有使用的索引。
索引失效的原因:
在这里插入图片描述
需要特别提出的是,关于字段区分性很差的情况,在加索引的时候就应该进行评估。如果区分性很差,这个索引根本就没必要加

区分性很差是什么意思?
1、某个字段只可能有 3 个值,那这个字段的索引区分度就很低,例如性别字段区分度就很低,要么是男要么是女。
2、再比如,某个字段大量为空,只有少量有值
3、再比如,某个字段值非常集中,90% 都是 1,剩下 10% 可能是 2,3,4…

进一步的,那如果不符合上面所有的索引失效的情况,但是 mysql 还是不使用对应的索引,是为啥呢?
这跟mysql的底层sql优化有关,mysql会在sql优化的时候先判断是否需要走索引,比如当数据量很少的时候,走全表扫描效率更高,走索引的话可能还要回表反而效率低。

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

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

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

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

如果必须要关联查询多张表,并且数据量很大的情况下,就需要在代码层面进行拆分优化,在业务层先查询一张表的数据,然后以关联字段作为查询关联表形成map。然后再业务层进行数据的组装。

⑤ in的元素过多
如果一个查询有 in,in 的条件加了合适的索引,这个时候的 sql 还是比较慢就可以高度怀疑是 in 的元素过多。
一旦排查出来是这个问题,解决起来也比较容易,不过是把元素分个组,每组查一次。想再快的话,可以再引入多线程。

1、进一步的,如果in的元素量大到一定程度还是快不起来,这种最好还是有个限制:

select id from person where id in(1,2,3......1000) limit 200

2、当然最好是在代码层面进行限制

if (ids.size() > 200) {
    throw new Exception("单次查询数据量不能超过200");
}

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

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

2、业务逻辑复杂

① 循环调用
这种情况,一般都循环调用同一段代码,每次循环的逻辑一致,前后不关联。
比如说,我们要初始化一个列表,预置 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(5, 5, 300L,
        TimeUnit.SECONDS, new LinkedBlockingQueue<>(10), commonThreadFactory, new ThreadPoolExecutor.DiscardPolicy());

// 开始多线程调用
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(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 两个逻辑可以并行执行,最大执行时间取决于哪个逻辑更慢。

3、线程池设计不合理

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

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

这三个参数是怎么打配合的呢?当线程池创建的时候,如果不预热线程池,则线程池中线程为 0。当有任务提交到线程池,则开始创建核心线程。
在这里插入图片描述
当核心线程全部被占满,如果再有任务到达,则让任务进入等待队列开始等待。
在这里插入图片描述
如果队列也被占满,则开始创建非核心线程运行。
在这里插入图片描述
如果线程总数达到最大线程数,还有任务进来,则开始根据线程池抛弃策略开始抛弃任务。
在这里插入图片描述
那么这个运行原理与接口运行时间有什么关系呢?

  • 核心线程设置过小:核心线程设置过小则没有达到并行的效果。
  • 线程池公用,别的业务的任务执行时间太长,占用了核心线程,另一个业务的任务到达就直接进入了等待队列。
  • 任务太多,以至于占满了线程池,大量任务在队列中等待。

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

4、锁设计不合理

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

锁过粗则是另一种常见的锁设计不合理的情况,如果我们把锁包裹的范围过大,则加锁时间会过长

public synchronized void doSome() {
    File f = calData();
    uploadToS3(f);
    sendSuccessMessage();
}

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

public void doSome() {
    File f = null;
    synchronized(this) {
        f = calData();
    }
    uploadToS3(f);
    sendSuccessMessage();
}
5、使用缓存

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值