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;
通过 ref
、key
、key_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/