前言
接口性能优化是后端开发人员经常碰到的一道面试题,由于它是一个跟开发语言无关的公共题目。
这个题目既可以很简朴,也可以相当复杂。
有时间,只必要添加一个索引就能解决。
有时间,代码必要进行重构。
有时间,必须增加缓存。
有时间,必要引入一些中间件,比方消息队列(MQ)。
有时间,需进行分库分表。
有时间,必要拆分服务。
等等。
导致接口性能题目的缘故起因多种多样,不同项目标不同接口,其缘故起因大概各不相同。
这里,小北给大家做一下体系化、体系化的梳理,使大家在面试过程中可以或许清晰、有条理的回答出面试官的提问,让面试官 “面前一亮、口水直流”,然后实现”offer直提”。
插播一条:假如你近期准备面试跳槽,发起在cxykk.com在线刷题,涵盖 1万+ 道 Java 面试题,险些覆盖了所有主流技能面试题、简历模板、算法刷题。
一、索引优化
接口性能优化时,大家第一个想到的通常是:优化索引。
确实,优化索引的成本是最小的。
你可以通过检察线上日志或监控报告,发现某个接口使用的某条SQL语句耗时较长。
此时,你大概会有以下疑问:
这条SQL语句是否已经加了索引?
加的索引是否生效了?
MySQL是否选择了错误的索引?
1.1 没加索引
在SQL语句中,忘记为WHERE条件的关键字段或ORDER BY后的排序字段加索引是项目中常见的题目。
在项目初期,由于表中的数据量较小,加不加索引对SQL查询性能影响不大。
然而,随着业务的发展,表中的数据量不断增加,这时就必须加索引了。
可以通过以下命令检察/添加索引:- show index from `table_name`
- CREATE INDEX index_name ON table_name (column_name);
复制代码 这种方式可以或许明显提高查询性能,尤其是在数据量庞大的情况下。
1.2 索引没生效
通过上述命令我们已经确认索引是存在的,但它是否生效呢?
此时,你大概会有这样的疑问。
那么,如何检察索引是否生效呢?
答案是:可以使用 EXPLAIN 命令,检察 MySQL 的执行筹划,它会体现索引的使用情况。
比方:- EXPLAIN SELECT * FROM `order` WHERE code='002';
复制代码 结果:
这个命令将体现查询的执行筹划,包罗使用了哪些索引。
假如索引生效,你会在输出结果中看到相关的信息。
通过这几列可以判断索引使用情况,执行筹划包罗列的含义如下图所示:
说实话,SQL语句没有使用索引,撤除没有建索引的情况外,最大的大概性是索引失效了。
以下是索引失效的常见缘故起因:
了解这些缘故起因,可以帮助你在查询优化时克制索引失效的题目,确保数据库查询性能保持最佳。
1.3 选错索引
此外,你是否遇到过这样一种情况:明明是同一条SQL语句,只是入参不同。
有时间使用的是索引A,有时间却使用索引B?
没错,有时间MySQL会选错索引。
必要时可以使用 FORCE INDEX 来强制查询SQL使用某个索引。
比方:- SELECT * FROM `order` FORCE INDEX (index_name) WHERE code='002';
复制代码 至于为什么MySQL会选错索引,缘故起因大概有以下几点:
了解这些缘故起因,可以帮助你更好地明白和控制MySQL的索引选择行为,确保查询性能的稳定性。
插播一条:假如你近期准备面试跳槽,发起在cxykk.com在线刷题,涵盖 1万+ 道 Java 面试题,险些覆盖了所有主流技能面试题、简历模板、算法刷题。
二、SQL优化
假如优化了索引之后效果不明显,接下来可以尝试优化一下SQL语句,由于相对于修改Java代码来说,改造SQL语句的成本要小得多。
以下是SQL优化的15个小本领:
三、长途调用
多时间,我们必要在一个接口中调用其他服务的接口。
比方,有这样的业务场景:
在用户信息查询接口中必要返回以下信息:用户名称、性别、等级、头像、积分和发展值。
其中,用户名称、性别、等级和头像存储在用户服务中,积分存储在积分服务中,发展值存储在发展值服务中。为了将这些数据同一返回,我们必要提供一个额外的对外接口服务。
因此,用户信息查询接口必要调用用户查询接口、积分查询接口和发展值查询接口,然后将数据汇总并同一返回。
调用过程如下图所示:
调用长途接口总耗时 530ms = 200ms + 150ms + 180ms
显然这种串行调用长途接口性能是非常不好的,调用长途接口总的耗时为所有的长途接口耗时之和。
那么如何优化长途接口性能呢?
3.1 串行改并行
上面说到,既然串行调用多个长途接口性能很差,为什么不改成并行呢?
如下图所示:
调用长途接口的总耗时为200ms,这等于耗时最长的那次长途接口调用时间。
在Java 8之前,可以通过实现Callable接口来获取线程的返回结果。
在Java 8之后,可以通过CompletableFuture类来实现这一功能。
以下是一个使用CompletableFuture的示例:- public class RemoteServiceExample {
- public static void main(String[] args) throws ExecutionException, InterruptedException {
- // 调用用户服务接口
- CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> {
- // 模拟远程调用
- simulateDelay(200);
- return "User Info";
- });
- // 调用积分服务接口
- CompletableFuture<String> pointsFuture = CompletableFuture.supplyAsync(() -> {
- // 模拟远程调用
- simulateDelay(150);
- return "Points Info";
- });
- // 调用成长值服务接口
- CompletableFuture<String> growthFuture = CompletableFuture.supplyAsync(() -> {
- // 模拟远程调用
- simulateDelay(100);
- return "Growth Info";
- });
- // 汇总结果
- CompletableFuture<Void> allOf = CompletableFuture.allOf(userFuture, pointsFuture, growthFuture);
- // 等待所有异步操作完成
- allOf.join();
- // 获取结果
- String userInfo = userFuture.get();
- String pointsInfo = pointsFuture.get();
- String growthInfo = growthFuture.get();
- }
- }
复制代码 3.2 数据异构
为了提升接口性能,尤其在高并发场景下,可以考虑数据冗余,将用户信息、积分和发展值的数据同一存储在一个地方,比如Redis。
这样,通过用户ID可以直接从Redis中查询所需的数据,从而克制长途接口调用
但必要注意的是,假如使用了数据异构方案,就大概会出现数据同等性题目。
用户信息、积分和发展值有更新的话,大部分情况下,会先更新到数据库,然后同步到redis。
但这种跨库的操作,大概会导致双方数据不同等的情况产生。
那如何解决数据同等性题目呢?
由于篇幅有限,本文就不展开具体说这块了,感兴趣的同学可以看我的另一篇文章《数据同等性》
四、重复调用
在我们的日常工作代码中,重复调用非经常见,但假如没有控制好,会严重影响接口的性能。
让我们一起来看看这个题目。
4.1 循环查数据库
有时间,我们必要从指定的用户集合中查询出哪些用户已经存在于数据库中。
一种实现方式如下:- public List<User> findExistingUsers(List<String> userIds) {
- List<User> existingUsers = new ArrayList<>();
- for (String userId : userIds) {
- User user = userRepository.findById(userId);
- if (user != null) {
- existingUsers.add(user);
- }
- }
- return existingUsers;
- }
复制代码 上述代码会对每个用户ID执行一次数据库查询,这在用户集合较大时会导致性能题目。
那么,我们如何优化呢?
我们可以通过批量查询来优化性能,淘汰数据库的查询次数。- public List<User> findExistingUsers(List<String> userIds) {
- // 批量查询数据库
- List<User> users = userRepository.findByIds(userIds);
- return users;
- }
复制代码这里有个必要注意的地方是:id集合的巨细要做限制,最好一次不要请求太多的数据。要根据实际情况而定,发起控制每次请求的记录条数在500以内。
五、异步处理
在进行接口性能优化时,有时间必要重新梳理业务逻辑,检查是否存在设计不公道的地方。
假设有一个用户请求接口,必要执行以下操作:
- 业务操作
- 发送站内关照
- 记录操作日志
为了实现方便,通常会将这些逻辑放在接口中同步执行,但这会对接口性能造成一定影响。
这个接口表面上看起来没有题目,但假如你仔细梳理一下业务逻辑,会发现只有业务操作才是核心逻辑,其他的功能都是非核心逻辑。
在这里有个原则就是:
核心逻辑可以同步执行,同步写库。非核心逻辑,可以异步执行,异步写库。
上面这个例子中,发站内关照和用户操作日志功能,对实时性要求不高,即使晚点写库,用户无非是晚点收到站内关照,或者运营晚点看到用户操作日志,对业务影响不大,所以完全可以异步处理。
异步处理方案
异步处理通常有两种主要方式:多线程和消息队列(MQ)
5.1 线程池异步优化
使用线程池改造之后,接口逻辑如下
5.2 MQ异步
使用线程池有个小题目就是:假如服务器重启了,或者是必要被执行的功能出现异常了,无法重试,会丢数据。
为了克制使用线程池处理异步任务时出现数据丢失的题目,可以考虑使用更加结实和可靠的异步处理方案,如消息队列(MQ)。消息队列不但可以异步处理任务,还可以或许保证消息的持久化和可靠性,支持重试机制。
使用mq改造之后,接口逻辑如下:
插播一条:假如你近期准备面试跳槽,发起在cxykk.com在线刷题,涵盖 1万+ 道 Java 面试题,险些覆盖了所有主流技能面试题、简历模板、算法刷题。
六、克制大事务
很多小伙伴在使用Spring框架开发项目时,为了方便,喜好使用@Transactional注解提供事务功能。
没错,使用@Transactional注解这种声明式事务的方式提供事务功能,确实能少写很多代码,提升开发效率。
但也容易造成大事务,引发性能的题目。
那么我们该如何优化大事务呢?
为了克制大事务引发的题目,可以考虑以下优化发起:
- 少用@Transactional注解
- 将查询(select)方法放到事务外
- 事务中克制长途调用
- 事务中克制一次性处理太多数据
- 有些功能可以非事务执行
- 有些功能可以异步处理
七、锁粒度
在一些业务场景中,为了克制多个线程并发修改同一共享数据而引发数据异常,通常我们会使用加锁的方式来解决这个题目。
然而,假如锁的设计不妥,导致锁的粒度过粗,也会对接口性能产生明显的负面影响。
7.1 synchronized
在Java中,我们可以使用synchronized关键字来为代码加锁。
通常有两种写法:在方法上加锁和在代码块上加锁。
1. 方法上加锁- public synchronized void doSave(String fileUrl) {
- mkdir();
- uploadFile(fileUrl);
- sendMessage(fileUrl);
- }
复制代码 在方法上加锁的目标是为了防止并发情况下创建相同的目录,克制第二次创建失败而影响业务功能。
但这种直接在方法上加锁的方式,锁的粒度较粗。
由于doSave方法中的文件上传和消息发送并不必要加锁,只有创建目录的方法必要加锁。
我们知道,文件上传操作非常耗时,假如将整个方法加锁,那么必要等到整个方法执行完之后才气释放锁。
显然,这会导致该方法的性能下降,得不偿失。
2. 代码块上加锁
我们可以将加锁改在代码块上,从而缩小锁的粒度,
如下:- public void doSave(String path, String fileUrl) {
- synchronized(this) {
- if (!exists(path)) {
- mkdir(path);
- }
- }
- uploadFile(fileUrl);
- sendMessage(fileUrl);
- }
复制代码 这样改造后,锁的粒度变小了,只有并发创建目录时才加锁。
创建目录是一个非常快的操作,即使加锁对接口性能的影响也不大。
最重要的是,其他的文件上传和消息发送功能仍然可以并发执行。
多节点情况中的题目
在单机版服务中,这种做法没有题目。但在生产情况中,为了保证服务的稳定性,同一个服务通常会部署在多个节点上。假如某个节点挂掉,其他节点的服务仍然可用。
多节点部署克制了某个节点挂掉导致服务不可用的情况,同时也能分摊整个体系的流量,克制体系压力过大。
但这种部署方式也带来了新的题目:synchronized只能保证一个节点加锁有效。
假如有多个节点,如何加锁呢?
7.2 Redis分布式锁
在分布式体系中,由于Redis分布式锁的实现相对简朴且高效,因此它在很多实际业务场景中被广泛接纳。
使用Redis分布式锁的伪代码如下:- public boolean doSave(String path, String fileUrl) {
- try {
- String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
- if ("OK".equals(result)) {
- if (!exists(path)) {
- mkdir(path);
- uploadFile(fileUrl);
- sendMessage(fileUrl);
- }
- return true;
- }
- } finally {
- unlock(lockKey, requestId);
- }
- return false;
- }
复制代码 与之前使用synchronized关键字加锁时一样,这里的锁的范围也太大了,换句话说,锁的粒度太粗。这会导致整个方法的执行效率很低。
实际上,只有在创建目录时才必要加分布式锁,别的代码不必要加锁。
于是,我们必要优化代码:- public void doSave(String path, String fileUrl) {
- if (tryLock()) {
- try {
- if (!exists(path)) {
- mkdir(path);
- }
- } finally {
- unlock(lockKey, requestId);
- }
- }
- uploadFile(fileUrl);
- sendMessage(fileUrl);
- }
- private boolean tryLock() {
- String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
- return "OK".equals(result);
- }
- private void unlock(String lockKey, String requestId) {
- // 解锁逻辑
- }
复制代码 上面的代码将加锁的范围缩小了,只有在创建目录时才加锁。这样的简朴优化后,接口性能可以得到明显提升。
7.3 数据库锁
MySQL数据库中的三种锁
- 表锁:
- 长处:加锁快,不会出现死锁。
- 缺点:锁定粒度大,锁冲突的概率高,并发度最低。
- 行锁:
- 长处:锁定粒度最小,锁冲突的概率低,并发度最高。
- 缺点:加锁慢,会出现死锁。
- 间隙锁:
- 长处:锁定粒度介于表锁和行锁之间。
- 缺点:开销和加锁时间介于表锁和行锁之间,并发度一般,也会出现死锁。
锁与并发度
并发度越高,接口性能越好。因此,数据库锁的优化方向是:
插播一条:假如你近期准备面试跳槽,发起在cxykk.com在线刷题,涵盖 1万+ 道 Java 面试题,险些覆盖了所有主流技能面试题、简历模板、算法刷题。
八、分页处理
有时间,我必要调用某个接口来批量查询数据,比方,通过用户ID批量查询用户信息,然后为这些用户赠送积分。
但是,假如一次性查询的用户数量太多,比方一次查询2000个用户的数据,传入2000个用户的ID进行长途调用时,用户查询接口经常会出现超时的情况。
调用代码如下:- List<User> users = remoteCallUser(ids);
复制代码 众所周知,调用接口从数据库获取数据必要经过网络传输。假如数据量过大,无论是数据获取速度还是网络传输速度都会受到带宽限制,从而导致耗时较长。
那么,这种情况下该如何优化呢?
答案是:分页处理。
将一次性获取所有数据的请求,改为分多次获取,每次只获取一部分用户的数据,最后进行合并和汇总。
其实,处理这个题目可以分为两种场景:同步调用和异步调用。
8.1 同步调用
假如在job中必要获取2000个用户的信息,它要求只要能正确获取到数据即可,对获取数据的总耗时要求不高。
但对每一次长途接口调用的耗时有要求,不能大于500ms,否则会有邮件预警。
这时,我们可以同步分页调用批量查询用户信息接口。
具体示例代码如下:- List<List<Long>> allIds = Lists.partition(ids, 200);
- for (List<Long> batchIds : allIds) {
- List<User> users = remoteCallUser(batchIds);
- }
复制代码 代码中我使用了Google Guava工具中的Lists.partition方法,用它来做分页简直太好用了,不然要写一大堆分页的代码。
8.2 异步调用
假如是在某个接口中必要获取2000个用户的信息,必要考虑的因素更多。
除了长途调用接口的耗时,还必要考虑该接口本身的总耗时,也不能凌驾500ms。
这时,使用上面的同步分页请求长途接口的方法肯定是行不通的。
那么,只能使用异步调用了。
代码如下:- List<List<Long>> allIds = Lists.partition(ids, 200);
- final List<User> result = Lists.newArrayList();
- allIds.stream().forEach(batchIds -> {
- CompletableFuture.supplyAsync(() -> {
- result.addAll(remoteCallUser(batchIds));
- return Boolean.TRUE;
- }, executor);
- });
复制代码 使用CompletableFuture类,通过多个线程异步调用长途接口,最后汇总结果同一返回。
九、加缓存
通常情况下,我们最常用的缓存是:Redis和Memcached。
但对于Java应用来说,绝大多数情况下使用的是Redis,所以接下来我们以Redis为例。
在关系型数据库(比方:MySQL)中,菜单通常有上下级关系。某个四级分类是某个三级分类的子分类,三级分类是某个二级分类的子分类,而二级分类又是某个一级分类的子分类。
这种存储结构决定了,想一次性查出整个分类树并非易事。这必要使用步伐递归查询,而假如分类很多,这个递归操作会非常耗时。
因此,假如每次都直接从数据库中查询分类树的数据,会是一个非常耗时的操作。
这时我们可以使用缓存。在大多数情况下,接口直接从缓存中获取数据。操作Redis可以使用成熟的框架,比如:Jedis和Redisson等。
使用Jedis的伪代码如下:- String json = jedis.get(key);
- if (StringUtils.isNotEmpty(json)) {
- CategoryTree categoryTree = JsonUtil.toObject(json);
- return categoryTree;
- }
- return queryCategoryTreeFromDb();
复制代码注意引入缓存之后,我们的体系复杂度就上升了,这时间就会存在数据不同等的题目
如何解决数据不同等的题目,感兴趣的小伙伴可以看我的另一篇文章,《》
十、分库分表
有时间,接口性能受限的并不是其他方面,而是数据库。
当体系发展到一定阶段,用户并发量增加,会有大量的数据库请求,这不但必要占用大量的数据库连接,还会带来磁盘IO的性能瓶颈题目。
此外,随着用户数量的不断增加,产生的数据量也越来越大,一张表大概无法存储所有数据。由于数据量太大,即使SQL语句使用了索引,查询数据时也会非常耗时。
那么,这种情况下该怎么办呢?
答案是:必要进行分库分表。
如下图所示:
图中将用户库拆分成了三个库,每个库都包罗了四张用户表。
假如有用户请求过来,先根据用户ID路由到其中一个用户库,然后再定位到某张表。
路由的算法有很多:
- 根据ID取模:
- 比方:ID=7,有4张表,则7%4=3,模为3,路由到用户表3。
- 给ID指定一个区间范围:
- 比方:ID的值是0-10万,则数据存在用户表0;ID的值是10-20万,则数据存在用户表1。
- 同等性Hash算法。
分库分表主要有两个方向:垂直和水平。
1. 垂直分库分表
垂直分库分表(即业务方向)更简朴,将不同的业务数据存储在不同的库或表中。
比方,将用户数据和订单数据存储在不同的库中。
2. 水中分库分表
水中分库分表(即数据方向)上,分库和分表的作用有区别,不能等量齐观。
分库
- 目标:解决数据库连接资源不足题目和磁盘IO的性能瓶颈题目。
分表
- 目标:解决单表数据量太大,SQL语句查询数据时,即使走了索引也非常耗时的题目。此外,还可以解决斲丧CPU资源的题目。
分库分表
- 目标:综合解决数据库连接资源不足、磁盘IO性能瓶颈、数据检索耗时和CPU资源斲丧等题目。
业务场景中的应用
十一、监控功能
优化接口性能题目,除了上面提到的这些常用方法之外,还必要配合使用一些辅助功能,由于它们真的可以帮我们提升查找题目的效率。
11.1 开启慢查询日志
通常情况下,为了定位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。
11.2 加监控
为了在出现SQL题目时可以或许及时发现,我们必要对体系做监控。
目前业界使用比较多的开源监控体系是:Prometheus。
它提供了监控和预警的功能。
架构图如下:
我们可以用它监控如下信息:
- 接口相应时间
- 调用第三方服务耗时
- 慢查询sql耗时
- cpu使用情况
- 内存使用情况
- 磁盘使用情况
- 数据库使用情况
- 等等。。。
它的界面大概长这样子:
可以看到MySQL的当前QPS、活跃线程数、连接数、缓存池的巨细等信息。
假如发现连接池占用的数据量太多,肯定会对接口性能造成影响。
这时大概是由于代码中开启了连接却忘记关闭,或者并发量太大导致的,必要进一步排查和体系优化
链路跟踪
有时间,一个接口涉及的逻辑非常复杂,比方查询数据库、查询Redis、长途调用接口、发送MQ消息以及执行业务代码等等。
这种情况下,接口的一次请求会涉及到非常长的调用链路。假如逐一排查这些题目,会泯灭大量时间,此时我们已经无法用传统的方法来定位题目。
有没有办法解决这个题目呢?
答案是使用分布式链路跟踪体系:SkyWalking。
SkyWalking的架构图如下:
在SkyWalking中,可以通过traceId(全局唯一的ID)来串联一个接口请求的完备链路。你可以看到整个接口的耗时、调用的长途服务的耗时、访问数据库或者Redis的耗时等,功能非常强大。
之前没有这个功能时,为了定位线上接口性能题目,我们必要在代码中加日志,手动打印出链路中各个环节的耗时情况,然后再逐一排查。这种方法不但费时费力,而且容易遗漏细节。
假如你用过SkyWalking来排查接口性能题目,你会不自觉地爱上它的功能。假如你想了解更多功能,可以访问SkyWalking的官网:skywalking.apache.org。
总结
认真看到这里的同学,相信已经对API接口性能优化有一个清晰的、体系的认知了,假如在面试中可以或许完备的说出这11种API接口性能优化的思路,相信面试官一定会对你刮目相看的。
最后说一句(求关注,求赞,别白嫖我)
最近无意间获得一份阿里大佬写的刷题条记,一下子打通了我的任督二脉,进大厂原来没那么难。
这是大佬写的, 7701页的BAT大佬写的刷题条记,让我offer拿到手软
本文,已收录于,我的技能网站 cxykk.com:步伐员编程资料站,有大厂完备面经,工作技能,架构师发展之路,等履历分享
求一键三连:点赞、分享、收藏
点赞对我真的非常重要!在线求赞,加个关注我会非常感激!
真的免费,假如你近期准备面试跳槽,发起在cxykk.com在线刷题,涵盖 1万+ 道 Java 面试题,险些覆盖了所有主流技能面试题、简历模板、算法刷题
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |