接口性能优化是后端开发人员经常碰到的一道面试题,因为它是一个跟开发语言无关的公共问题。
这个问题既可以很简单,也可以相当复杂。
导致接口性能问题的原因多种多样,不同项目的不同接口,其原因可能各不相同。
下面列举几种常见的性能优化方案:
一、索引优化
不管是查询、新增、修改,一个接口必然会去后端请求数据库,所以直接从sql层面进行优化是在设计时必须考虑的一方面。如果查询的时候经常使用的某些字段没有添加索引,就可以根据条件给某些字段添加索引,然后在看sql的具体耗时。
如果添加上了索引,要看一下这个索引有没有生效要怎么看呢?
答案是:可以使用 EXPLAIN 命令,查看 MySQL 的执行计划,它会显示索引的使用情况。
这个命令将显示查询的执行计划,包括使用了哪些索引。
如果索引生效,你会在输出结果中看到相关的信息。
通过这几列可以判断索引使用情况,执行计划包含列的含义如下图所示:
SQL语句没有使用索引,除去没有建索引的情况外,最大的可能性是索引失效了。
以下是索引失效的常见原因:
二、SQL优化
如果优化了索引之后效果不明显,接下来可以尝试优化一下SQL语句,因为相对于修改Java代码来说,改造SQL语句的成本要小得多。
以下是SQL优化的15个小技巧:
三、远程调用
有时候,我们需要在一个接口中调用其他多个服务的接口。
例如,有这样的业务场景:
在用户信息查询接口中需要返回以下信息:用户名称、性别、等级、头像、积分和成长值。
其中,用户名称、性别、等级和头像存储在用户服务 中,积分存储在积分服务 中,成长值存储在成长值服务中。为了将这些数据统一返回,我们需要提供一个额外的对外接口服务。
因此,用户信息查询接口需要调用用户查询接口、积分查询接口和成长值查询接口,然后将数据汇总并统一返回。
调用过程如下图所示:
调用远程接口总耗时 530ms = 200ms + 150ms + 180ms
显然这种串行调用远程接口性能是非常不好的,调用远程接口总的耗时为所有的远程接口耗时之和。
(1)串行改并行
调用远程接口的总耗时为200ms,这等于耗时最长的那次远程接口调用时间。
在Java 8之前,可以通过实现Callable接口来获取线程的返回结果。
在Java 8之后,可以通过CompletableFuture类来实现这一功能。
以下是一个使用CompletableFuture的示例:
(2)数据异构
为了提升接口性能,尤其在高并发场景下,可以考虑数据冗余,将用户信息、积分和成长值的数据统一存储在一个地方,比如Redis。
这样,通过用户ID可以直接从Redis中查询所需的数据,从而避免远程接口调用。
但需要注意的是,如果使用了数据异构方案,就可能会出现数据一致性问题。
用户信息、积分和成长值有更新的话,大部分情况下,会先更新到数据库,然后同步到redis。
但这种跨库的操作,可能会导致两边数据不一致的情况产生。
redis和数据库如何保证一致性呢?
为了保证Redis和数据库之间的数据一致性,可以采用以下策略:
1)基于事务的一致性: 使用数据库的事务来确保数据库操作的一致性。在操作Redis和数据库时,要么两者都成功,要么都失败,使用数据库的事务可以保证这一点。
2)使用锁: 在并发情况下,可以使用锁来确保数据的一致性。当操作Redis和数据库时,首先获取锁,然后执行操作,最后释放锁。
3)读写直写策略: 在更新数据库后,同时更新Redis。这样,当读取数据时,可以直接从Redis获取,避免了数据库的IO开销。
4)异步串行化: 当更新数据库时,通过消息队列异步通知Redis更新,保证Redis的数据最终是一致的,但不保证实时一致性。
5)失败重试和回滚: 如果Redis或数据库的更新失败,应该有重试机制,并在必要时实现回滚操作。
四、重复调用
在我们的日常工作代码中,重复调用非常常见,但如果没有控制好,会严重影响接口的性能。
解决方案:可以通过批量查询来优化性能,减少数据库的查询次数。
五、异步处理
核心逻辑可以同步执行,同步写库。非核心逻辑,可以异步执行,异步写库。
异步处理方案
异步处理通常有两种主要方式:多线程和消息队列(MQ)
六、避免大事务
大事务,引发性能的问题
那么我们该如何优化大事务呢?
为了避免大事务引发的问题,可以考虑以下优化建议:
少用@Transactional注解
将查询(select)方法放到事务外
事务中避免远程调用
事务中避免一次性处理太多数据
有些功能可以非事务执行
有些功能可以异步处理
七、锁粒度
在一些业务场景中,为了避免多个线程并发修改同一共享数据而引发数据异常,通常我们会使用加锁的方式来解决这个问题。
然而,如果锁的设计不当,导致锁的粒度过粗,也会对接口性能产生显著的负面影响。
这里说一下MySQL数据库中的三种锁
表锁:
优点:加锁快,不会出现死锁。
缺点:锁定粒度大,锁冲突的概率高,并发度最低。
行锁:
优点:锁定粒度最小,锁冲突的概率低,并发度最高。
缺点:加锁慢,会出现死锁。
间隙锁:
优点:锁定粒度介于表锁和行锁之间。
缺点:开销和加锁时间介于表锁和行锁之间,并发度一般,也会出现死锁。
锁与并发度
并发度越高,接口性能越好。因此,数据库锁的优化方向是:
优先使用行锁
其次使用间隙锁
最后使用表锁
八、分页处理
调用接口从数据库获取数据需要经过网络传输。如果数据量过大,无论是数据获取速度还是网络传输速度都会受到带宽限制,从而导致耗时较长。
那么,这种情况下该如何优化呢?
答案是:分页处理。
将一次性获取所有数据的请求,改为分多次获取,每次只获取一部分用户的数据,最后进行合并和汇总。
九、加缓存
通常情况下,我们最常用的缓存是:Redis和Memcached。将一些经常查询需要的固定不变的数据加入到缓存中,请求的时候优先从缓存里面去取数据,这样会大大提高接口的查询性能,因为缓存是存在内存中的,避免了大量的数据库查询。
十、分库分表
有时候,接口性能受限的并不是其他方面,而是数据库。
当系统发展到一定阶段,用户并发量增加,会有大量的数据库请求,这不仅需要占用大量的数据库连接,还会带来磁盘IO的性能瓶颈问题。
此外,随着用户数量的不断增加,产生的数据量也越来越大,一张表可能无法存储所有数据。由于数据量太大,即使SQL语句使用了索引,查询数据时也会非常耗时。
那么,这种情况下该怎么办呢?
答案是:需要进行分库分表。
十一、监控功能
优化接口性能问题,除了上面提到的这些常用方法之外,还需要配合使用一些辅助功能,因为它们真的可以帮我们提升定位问题的效率。
开启慢查询日志
通常情况下,为了定位SQL的性能瓶颈,我们需要开启MySQL的慢查询日志。把超过指定时间的SQL语句单独记录下来,方便以后分析和定位问题。
开启慢查询日志需要重点关注三个参数:
slow_query_log:慢查询开关
slow_query_log_file:慢查询日志存放的路径
long_query_time:超过多少秒才会记录日志
通过MySQL的SET命令可以设置:
加监控
为了在出现SQL问题时能够及时发现,我们需要对系统做监控。
目前业界使用比较多的开源监控系统是:Prometheus。
它提供了监控和预警的功能。
监控如下信息:
接口响应时间
调用第三方服务耗时
慢查询sql耗时
cpu使用情况
内存使用情况
磁盘使用情况
数据库使用情况
链路跟踪
有时候,一个接口涉及的逻辑非常复杂,例如查询数据库、查询Redis、远程调用接口、发送MQ消息以及执行业务代码等等。
这种情况下,接口的一次请求会涉及到非常长的调用链路。如果逐一排查这些问题,会耗费大量时间,此时我们已经无法用传统的方法来定位问题。
有没有办法解决这个问题呢?
答案是使用分布式链路跟踪系统:SkyWalking。
在SkyWalking中,可以通过traceId(全局唯一的ID)来串联一个接口请求的完整链路。你可以看到整个接口的耗时、调用的远程服务的耗时、访问数据库或者Redis的耗时等,功能非常强大。