接口优化

Web开发中,后端主要的工作就是写接口,随着项目的发展和系统集成,接口的性能也需要优化。

一般导致接口性能问题的原因不尽相同,项目功能不同的接口,导致接口出现性能问题的原因可能也不一样,要根据场景来分享,即具体情况具体分析。

哪些问题会引起接口性能问题?

慢查询(基于mysql)

分页

所谓的深度分页问题,涉及到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以参数的方式传给接口提供方,会有沟通成本(调用方:老子不改!)。

未加索引

在平时项目中比较常见的问题:就是在 sql 语句中 where 条件的关键字段,或者 order by 后面的排序字段,漏加索引。

当然项目初期体量比较小,表中的数据量小,加不加索引 sql 查询性能差别不大,没啥影响。

随后,如果业务发展起来了,表中数据量也越来越多,此时就不得不加索引了。

show create table xxxx(表名)

查看某张表的索引。

具体加索引的语句网上太多了,不再赘述。

不过顺便提一嘴,加索引之前,需要考虑一下这个索引是不是有必要加,如果加索引的字段区分度非常低,那即使加了索引也不会生效。

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

索引失效

这个是慢查询最不好分析的情况,虽然mysql提供了explain来评估某个sql的查询性能,其中就有使用的索引。

但是为啥索引会失效呢?

mysql却不会告诉咱,需要咱自己分析。

大体上,可能引起索引失效的原因有这几个(可能不完全):

在已经能够确认索引有的情况下,接下来需要关注它是否生效了?

首先我们可以使用 mysql 的 explain 命令来查看 sql 的执行计划,它会显示索引的使用情况。

explain select * from `t_order` where Fdeal_id=1001;

通过 refkeykey_len 这几列可以知道索引使用情况,执行计划包含列的含义如下图所示:

explain 执行计划中包含关键的信息如下:

  • select_type: 查询类型
  • table: 表名或者别名
  • partitions: 匹配的分区
  • type: 访问类型
  • possible_keys: 可能用到的索引
  • key: 实际用到的索引
  • key_len: 索引长度
  • ref: 与索引比较的列
  • rows: 估算的行数
  • filtered: 按表条件筛选的行百分比

下面列举了常见索引失效的原因:

  • 不满足最左前缀原则
  • 使用了 select *
  • 使用索引列时进行计算
  • 范围索引没有放后面
  • 字符类型没有加引号
  • 索引列上使用了函数
  • like 查询左侧有%
  • 等等

如果区分性很差,这个索引根本就没必要加。区分性很差是什么意思呢,举几个例子,比如:

  • 某个字段只可能有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 ...... 1000) limit 200

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

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

单纯的数据量过大

这种问题,单纯代码的修修补补一般就解决不了了,需要变动整个的数据存储架构。

或者是对底层mysql分表或分库+分表;或者就是直接变更底层数据库,把mysql转换成专门为处理大数据设计的数据库。

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

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

使用远程调用RPC

在大多数时候,项目中往往需要在某个接口中,调用其它服务的接口。

比如商城的业务场景:

下单时需要调用用户信息接口,在用户信息查询接口中需要返回:用户名称、性别、等级、头像、积分、成长值等信息,另外也需要调用商品信息接口,在用户信息查询接口中需要返回:商品主图链接、价格、活动等信息。而积分在积分服务中,活动在活动服务中。

因此,为了汇总这些数据统一返回,需要另外提供一个对外接口的服务。

于是,用户信息查询接口就需要调用用户查询接口、积分查询接口和活动接口,然后汇总数据统一返回。

可以知道远程调用接口总耗时为:450ms = 150ms + 100ms + 200ms.

很明显这种串行远程调用接口性能是很差的,效率也非常低,远程调用接口的总耗时为调用各个远程接口耗时之和。

那么如何优化远程调用接口的性能呢?继续往下看。

使用缓存

我们可以考虑把数据冗余一下,把用户信息、积分和活动信息的数据统一存储到一个地方,比如:redis,存的数据结构就是用户信息查询接口所需要的内容。

接下来可以通过用户 id,直接从 redis 中查询出来,这大大提高了效率。

如果在高并发的场景下,为了提升接口性能,远程接口调用大概率会被去掉,而改成保存冗余数据的缓存方案。

但需要注意的是,如果使用了缓存方案,就要另外考虑数据一致性的问题。

用户信息、积分和活动信息更新的话,大部分情况下,会先更新到数据库,然后同步到 redis。但这种跨库的操作,可能会导致两边数据不一致的情况产生。

重复调用接口

在同一个接口中,重复调用在我们平时开发的代码中可以说随处可见,但是如果没有控制好的话,会大大影响接口的性能。

循环去查数据库

大多数时候,我们需要从指定的数据库集合中,查询出需要用到的数据。

当有多个用户 id 传多来时,如果每个用户 id 都需要查一遍的话,那么就需要循环多次去查询数据库了。我们都知道,每查询一次数据库,就会进行一次远程调用。这是非常耗时的操作。

那么,我们可以提供一个根据用户id 集合批量查询用户信息数据的接口,只需远程调用一次即可,就能查询出所需要的数据了。

这里温馨提示下:id 集合的大小需要做限制以及做入参校验,否则也会影响查询性能,最好一次不要请求太多的数据。可以根据业务实际情况而定。

避免出现死循环

有些时候,写代码一不留神,循环语句就出现死循环了。

出现这种情况往往就是 condition 条件没处理好,导致没有退出循环,从而导致死循环。

出现死循环,大概率是代码的 bug 导致的,不过这种情况很容易被测出来。

但是,可能还有一种比较隐秘的死循环代码,当用正常数据时,测不出问题,一旦出现有异常数据,才会复现死循环的问题。

避免无限递归

一些导致无限递归的场景以及影响接口性能程度这里就不啰嗦了,总之,在写递归代码时,建议设定一个递归的深度(假设限定为 5),然后在递归方法中做一定判断,如果深度大于 5 时,则自动返回,这样就可以避免无限递归了。

考虑使用异步处理

很多时候,在进行接口性能优化时,需要重新梳理一下业务逻辑,看看是否有设计上不太合理的地方。

比如有个用户请求接口中,需要做业务操作,发站内通知,和记录操作日志。

为了实现起来比较方便,通常我们会将这些逻辑放在接口中同步执行,势必会对接口性能造成一定的影响。

这样实现的接口表面上看起来没啥问题,但如果你仔细梳理一下业务逻辑,会发现只有业务操作才是核心逻辑,其他的功能都是非核心逻辑。

在这里有个原则就是:核心逻辑可以同步执行,同步写库。非核心逻辑,可以异步执行,异步写库

上面这个例子中,发站内通知和用户操作日志功能,对实时性要求不高,即使晚点写库,用户无非是晚点收到站内通知,或者运营晚点看到用户操作日志,对业务影响不大,所以完全可以异步处理。

通常异步主要有两种:多线程 和 mq。

数据库级别的锁

使用 mysql 数据库中锁主要有三种级别:

  • 表锁:加锁快,不会出现死锁。但锁定粒度大,发生锁冲突的概率最高,并发度最低。
  • 行锁:加锁慢,会出现死锁。但锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  • 间隙锁:开销和加锁时间界于表锁和行锁之间。它会出现死锁,锁定粒度界于表锁和行锁之间,并发度一般。

如果并发度越高,意味着接口性能越好。所以数据库锁的优化方向是:优先使用行锁,其次使用间隙锁,再其次使用表锁。

考虑是否要分库分表

有些时候,接口性能受限的不是别的,而是数据库。

当系统发展到一定的阶段,用户并发量大,会有大量的数据库请求,需要占用大量的数据库连接,同时会带来磁盘IO的性能瓶颈问题。

此外,随着用户数量越来越多,产生的数据也越来越多,一张表有可能存不下。由于数据量太大,sql 语句查询数据时,即使走了索引也会非常耗时。

此时就需要考虑做分库分表了。

其它辅助优化接口功能

优化接口性能问题,除了上面提到的这些常用方法之外,还需要配合使用一些辅助功能,因为它们真的可以帮我们提升查找问题的效率。

开启慢查询日志

通常情况下,为了定位sql的性能瓶颈,我们需要开启 mysql 的慢查询日志。把超过指定时间的 sql 语句,单独记录下来,方面以后分析和定位问题。

开启慢查询日志需要重点关注三个参数:

  • slow_query_log 慢查询开关
  • slow_query_log_file 慢查询日志存放的路径
  • long_query_time 超过多少秒才会记录日志

通过 mysql 的 set 命令可以设置:

set global slow_query_log='ON'; 
set global slow_query_log_file='/usr/local/mysql/data/slow.log';
set global long_query_time=2;

设置完之后,如果某条sql的执行时间超过了 2 秒,会被自动记录到 slow.log 文件中。

当然也可以直接修改配置文件 my.cnf:

[mysqld]
slow_query_log = ON
slow_query_log_file = /usr/local/mysql/data/slow.log
long_query_time = 2

但这种方式需要重启mysql服务。 很多公司每天早上都会发一封慢查询日志的邮件,开发人员根据这些信息优化 sql。

加监控

为了出现sql问题时,能够让我们及时发现,我们需要对系统做监控。

目前业界使用比较多的开源监控系统是:Prometheus

它提供了 监控预警 的功能。 如果你想了解更多功能,可以访问 Prometheus 的官网:https://prometheus.io/

你可能感兴趣的:(接口优化)