总结接口性能优化的N个小技巧

1.索引

接口性能优化大家第一个想到的可能是:优化索引

没错,优化索引的成本是最小的。

你通过查看线上日志或者监控报告,查到某个接口用到的某条sql语句耗时比较长。

这时你可能会有下面这些疑问:

1. 该sql语句加索引了没?

2. 加的索引生效了没?

3. mysql选错索引了没?

4. 索引是不是不合理?

1.1 没加索引

sql语句中where条件的关键字段,或者order by后面的排序字段,忘了加索引,这个问题在项目中很常见。

项目刚开始的时候,由于表中的数据量小,加不加索引sql查询性能差别不大。

后来,随着业务的发展,表中数据量越来越多,就不得不加索引了。

可以通过命令:

show index from `order`;

能单独查看某张表的索引情况。

也可以通过命令:

show create table `order`;

查看整张表的建表语句,里面同样会显示索引情况。

通过ALTER TABLE命令可以添加索引:

ALTER TABLE `order` ADD INDEX idx_name (name);

也可以通过CREATE INDEX命令添加索引:

CREATE INDEX idx_name ON `order` (name);

不过这里有一个需要注意的地方是:想通过命令修改索引,是不行的。

目前在mysql中如果想要修改索引,只能先删除索引,再重新添加新的。

删除索引可以用DROP INDEX命令:

ALTER TABLE `order` DROP INDEX idx_name;

DROP INDEX命令也行:

DROP INDEX idx_name ON `order`;

1.2 索引没生效

通过上面的命令我们已经能够确认索引是有的,但它生效了没?此时你内心或许会冒出这样一个疑问。

那么,如何查看索引有没有生效呢?

答:可以使用explain命令,查看mysql的执行计划,它会显示索引的使用情况。

例如:

explain select * from `order` where code='002';

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

说实话,sql语句没有走索引,排除没有建索引之外,最大的可能性是索引失效了。

下面说说索引失效的常见原因: 如果不是上面的这些原因,则需要再进一步排查一下其他原因。

1.3 选错索引

此外,你有没有遇到过这样一种情况:明明是同一条sql,只有入参不同而已。有的时候走的索引a,有的时候却走的索引b?

没错,有时候mysql会选错索引。

必要时可以使用force index来强制查询sql走某个索引。

1.4 索引不合理

MySQL索引不合理的例子:

1. 在频繁更新的列上创建索引:如果一个列经常被修改,那么每次修改都会触发索引的重新构建,这将导致性能下降。例如,如果一个表中有一个名为`last_updated`的列,用于记录每行数据的最后更新时间,那么在这个列上创建索引可能不是一个好主意。

2. 在小表上创建过多的索引:虽然索引可以提高查询速度,但是过多的索引会增加写入操作的开销,因为每次插入或更新数据时,都需要更新索引。此外,索引还会占用额外的磁盘空间。因此,在小表上创建过多的索引可能是不合理的。

3. 在不经常使用的列上创建索引:如果一个列很少被用于查询条件,那么在这个列上创建索引可能不会带来明显的性能提升。例如,一个包含用户信息的表,其中有一个名为`middle_name`的列,这个列很少被用于查询,那么在这个列上创建索引可能是不合理的。

4. 在具有大量重复值的列上创建索引:如果一个列的值有很多重复,那么在这个列上创建索引可能不会提高查询性能。例如,一个包含员工信息的表,其中有一个名为`gender`的列,这个列的值只有两个(男和女),那么在这个列上创建索引可能不会带来明显的性能提升。

5. 在不适合使用索引的数据类型上创建索引:某些数据类型(如TEXT、BLOB)不适合创建索引,因为它们的数据量较大,而且不容易进行比较。在这些数据类型上创建索引可能会导致性能下降。

2. sql优化

如果优化了索引之后,也没啥效果。

接下来试着优化一下sql语句,因为它的改造成本相对于java代码来说也要小得多。

下面给大家列举了sql优化的15个小技巧:

3. 远程调用

很多时候,我们需要在某个接口中,调用其他服务的接口。

比如有这样的业务场景:

在用户信息查询接口中需要返回:用户名称、性别、等级、头像、积分、成长值等信息。

而用户名称、性别、等级、头像在用户服务中,积分在积分服务中,成长值在成长值服务中。为了汇总这些数据统一返回,需要另外提供一个对外接口服务。

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

调用过程如下图所示:

调用远程接口总耗时 530ms = 200ms + 150ms + 180ms

显然这种串行调用远程接口性能是非常不好的,调用远程接口总的耗时为所有的远程接口耗时之和。

那么如何优化远程接口性能呢?

3.1 并行调用

上面说到,既然串行调用多个远程接口性能很差,为什么不改成并行呢?

如下图所示:

调用远程接口总耗时 200ms = 200ms(即耗时最长的那次远程接口调用)

在java8之前可以通过实现Callable接口,获取线程返回结果。

java8以后通过CompleteFuture类实现该功能。我们这里以CompleteFuture为例:

// 定义一个方法,通过用户ID获取用户信息public UserInfo getUserInfo(Long id) throws InterruptedException, ExecutionException {   // 创建一个UserInfo对象用于存储用户信息   final UserInfo userInfo = new UserInfo();      // 异步获取远程用户信息并填充到userInfo对象中   CompletableFuture userFuture = CompletableFuture.supplyAsync(() -> {       getRemoteUserAndFill(id, userInfo);       return Boolean.TRUE;   }, executor);      // 异步获取远程奖金信息并填充到userInfo对象中   CompletableFuture bonusFuture = CompletableFuture.supplyAsync(() -> {       getRemoteBonusAndFill(id, userInfo);       return Boolean.TRUE;   }, executor);      // 异步获取远程成长信息并填充到userInfo对象中   CompletableFuture growthFuture = CompletableFuture.supplyAsync(() -> {       getRemoteGrowthAndFill(id, userInfo);       return Boolean.TRUE;   }, executor);      // 等待所有异步任务完成   CompletableFuture.allOf(userFuture, bonusFuture, growthFuture).join();      // 获取异步任务的结果(这里实际上不需要调用get()方法,因为已经使用join()等待了所有任务完成)   userFuture.get();   bonusFuture.get();   growthFuture.get();      // 返回填充好的userInfo对象   return userInfo;}

温馨提醒一下,这两种方式别忘了使用线程池。示例中我用到了executor,表示自定义的线程池,为了防止高并发场景下,出现线程过多的问题。

并行,串行与同步,异步

并行和串行指的是任务的执行方式。串行是指多个任务时,各个任务按顺序执行,完成一个之后才能进行下一个。并行指的是多个任务可以同时执行,异步是多个任务并行的前提条件。

同步不能开启新的线程,异步可以。

异步:异步和同步是相对的,同步就是顺序执行,执行完一个再执行下一个,需要等待、协调运行。异步就是彼此独立,在等待某事件的过程中继续做自己的事,不需要等待这一事件完成后再工作。线程就是实现异步的一个方式。异步是让调用方法的主线程不需要同步等待另一线程的完成,从而可以让主线程干其它的事情。

3.2 数据异构

上面说到的用户信息查询接口需要调用用户查询接口、积分查询接口 和 成长值查询接口,然后汇总数据统一返回。

那么,我们能不能把数据冗余一下,把用户信息、积分和成长值的数据统一存储到一个地方,比如:redis,存的数据结构就是用户信息查询接口所需要的内容。然后通过用户id,直接从redis中查询数据出来,不就OK了?

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

但需要注意的是,如果使用了数据异构方案,就可能会出现数据一致性问题。

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

4. 重复调用

重复调用在我们的日常工作代码中可以说随处可见,但如果没有控制好,会非常影响接口的性能。

4.1 循环操作数据库

有时候,我们需要从指定的用户集合中,查询出有哪些是在数据库中已经存在的。

实现代码可以这样写:

public List<User> queryUser(List<User> searchList) {

if (CollectionUtils.isEmpty(searchList)) {

return Collections.emptyList();

}

List<User> result = Lists.newArrayList();

searchList.forEach(user -> result.add(userMapper.getUserById(user.getId())));

return result;

}

这里如果有50个用户,则需要循环50次,去查询数据库。我们都知道,每查询一次数据库,就是一次远程调用。

如果查询50次数据库,就有50次远程调用,这是非常耗时的操作。

那么,我们如何优化呢?

具体代码如下:

public List<User> queryUser(List<User> searchList) {

if (CollectionUtils.isEmpty(searchList)) {

return Collections.emptyList();

}

List<Long> ids = searchList.stream().map(User::getId).collect(Collectors.toList());

return userMapper.getUserByIds(ids);

}

提供一个根据用户id集合批量查询用户的接口,只远程调用一次,就能查询出所有的数据。

这里有个需要注意的地方是:id集合的大小要做限制,最好一次不要请求太多的数据。要根据实际情况而定,建议控制每次请求的记录条数在500以内。

5. 异步处理

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

比如有个用户请求接口中,需要做业务操作,发站内通知,和记录操作日志。为了实现起来比较方便,通常我们会将这些逻辑放在接口中同步执行,势必会对接口性能造成一定的影响。

接口内部流程图如下:

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

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

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

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

5.1 线程池

使用线程池改造之后,接口逻辑如下:

发站内通知和用户操作日志功能,被提交到了两个单独的线程池中。

这样接口中重点关注的是业务操作,把其他的逻辑交给线程异步执行,这样改造之后,让接口性能瞬间提升了。

但使用线程池有个小问题就是:如果服务器重启了,或者是需要被执行的功能出现异常了,无法重试,会丢数据。

那么这个问题该怎么办呢?

5.2 mq

使用mq改造之后,接口逻辑如下:

对于发站内通知和用户操作日志功能,在接口中并没真正实现,它只发送了mq消息到mq服务器。然后由mq消费者消费消息时,才真正的执行这两个功能。

这样改造之后,接口性能同样提升了,因为发送mq消息速度是很快的,我们只需关注业务操作的代码即可。

6. 避免@Transactional大事务

很多小伙伴在使用spring框架开发项目时,为了方便,喜欢使用@Transactional注解提供事务功能。

没错,使用@Transactional注解这种声明式事务的方式提供事务功能,确实能少写很多代码,提升开发效率。

但也容易造成大事务,引发其他的问题。

所谓的大事务就是耗时比较长的事务。

大事务产生的原因

○ 操作的数据比较多

○ 调用了 rpc 方法

○ 有其他非 DB 的耗时操作

○ 大量的锁竞争

○ 执行了比较耗时的计算

大事务造成的影响

○ 并发情况下,数据库连接池容易被撑爆

○ 锁定太多的数据,造成大量的阻塞和锁超时

○ 执行时间长,容易造成主从延迟

○ 回滚所需要的时间比较长

○ undo log日志膨胀,不仅增加了存储的空间,而且可能降低查询的性能

大事务优化方案

处理大事务的6种办法:

1. 少用@Transactional注解

2. 将查询(select)方法放到事务外

3. 事务中避免远程调用

4. 事务中避免一次性处理太多数据

5. 非事务执行

6. 异步处理

● 1.使用编程式事务

在实际项目开发中,有多个写请求就需要用到事务,我们在业务方法加上@Transactional注解开启事务功能,这是非常普遍的做法,它被称为声明式事务

部分代码如下:

 @Transactional   public void save(User user) {         //doSameThing...   }

然而,我要说的第一条是:少用@Transactional注解。

为什么?

1. 我们知道@Transactional注解是通过spring的aop起作用的,但是如果使用不当,事务功能可能会失效(同一个类中方法内部直接调用:当一个类的方法内部直接调用标注有@Transactional的另一个方法时,事务是不会生效的。这是因为内部调用绕过了Spring的AOP代理机制,直接通过this对象调用,而不是通过代理对象调用)。如果恰巧你经验不足,这种问题不太好排查。

2. @Transactional 注解一般加在某个业务方法上,会导致整个业务方法都在同一个事务中,粒度太粗,不好控制事务范围,是出现大事务问题的最常见的原因。

那我们该怎么办呢?

可以使用编程式事务,在spring项目中使用TransactionTemplate类的对象,手动执行事务。

部分代码如下:

@Autowired    private TransactionTemplate transactionTemplate;  

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

本专栏价格永远为19.9元! 不想当架构师的后端开发工程师不是好码农! 此专栏一方面用于存放我的架构设计学习笔记, 另外我会在本专栏加入一系列最常问八股问题帖子,内容就是我根据自己的面试经历和网上的面经,去筛选八股里面哪些是最常被问到的问题把它们整理出来,大家可以在面试前一两个小时快速把这一系列最常问八股的帖子拿出来看看,临时抱佛脚的效果应该很好

全部评论
觉得文章有用的就动动byd小手点个赞吧!你的赞对我很重要!
2 回复 分享
发布于 2024-07-22 19:03 广东
能不能价格低一点 十块钱 让大家都买得起!
1 回复 分享
发布于 2024-09-04 14:15 北京

相关推荐

07-16 18:03
门头沟学院 Java
点赞 评论 收藏
分享
点赞 评论 收藏
分享
评论
7
16
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务