后端服务接口性能优化建议

概述

要想成为一名优秀的后端程序员,编写出高性能的服务接口是一个重要指标,高标准程序员都是对性能反复压榨的。以下梳理了一些提升接口性能的技术方案,希望对大家有所帮助。

6b02fa8c747525462b2620ca6e7c75ce.jpeg

1、数据库索引

当接口出现性能问题时,最容易想到的就是添加索引,索引优化是代价最小的优化,而且效果很明显。索引优化主要从以下几个角度考虑:

SQL是否添加索引?索引是否生效?索引设计是否合理?

1.1 SQL是否添加索引

开发阶段就要考虑数据库表的索引设计,对于一些经常作为检索条件、order by、group by 后面的字段,且数据区分度高的字段,可以考虑创建索引。

开发阶段就把创建索引脚本写好放在上线待执行脚本文件中,避免上线时忘记索引创建。

可以通过explain执行查询计划,查看SQL执行情况:

explain select * from user where name like '%张';

也可以通过命令show create table user,检查整张表的索引情况。

show create table user

如果某个表忘记添加某个索引,可以通过alter table add index命令添加索引。

alter table user add index idx_name (name);

注意:在数据量很大的表中创建索引,最好选择在业务不繁忙时间段,避免影响线上业务。

1.2 索引不生效

有时候虽然添加了索引,但是索引可能会失效,如下图梳理了索引失效的14种情况:

be21c98849d43594b5b9c1b9d8e5ae9f.jpeg

1.3 索引设计是否合理

索引不是设计越多越好,设计必须要合理,例如:

优先考虑设计联合索引,适当使用覆盖索引;索引个数尽量不要超过5个;索引最好选择数据区分度较高的字段,如性别太多重复字段就不适合创建索引;


2、慢SQL优化

在索引优化之后,还可以进一步优化慢SQL语句,如下梳理10条慢sql优化建议:

d80733db5be817d4a7caf68f4b056dbb.jpeg

3、异步执行

对于一些耗时操作或者不影响主要业务的逻辑,可以采用异步执行,来提升性能。

1096c34be2b8e55b9ffadbd09fd8182d.png

为了降低接口耗时,及时返回结果,可以把短信发送、日志写入及积分赠送通过异步执行。

类似的场景还有:用户下订单之后的消息发送及赠送积分也可以放到异步处理。

常见的异步实现:线程池、消息队列MQ、Spring注解@Async、异步框架CompletableFuture、Spring ApplicationEvent事件。

4、批量调用

数据库操作或或者远程调用时,能批量操作就不要for循环调用。

  • 一个简单例子,我们平时一个列表明细数据插入数据库时,不要在for循环一条一条插入,建议一个批次几百条,进行批量插入,减少多次IO,建议使用Mybatis 的foreach操作,不过数量也不要一次太多(100),MP的saveBatch、或者PreparedStatement 的addBatch()

  • 同理远程调用类似,比如你查询营销标签是否命中,可以一个标签一个标签去查,也可以批量标签去查,那批量进行,效率就更高。

//反例
for(int i=0;i<n;i++){
  singleUpdate(param)
}
//正例
batchUpdate(param);

打个比喻:

假如你需要搬一万块砖到一个地方,你有一辆汽车,汽车一次可以放适量的砖
(最多放800), 你可以选择一次运送一块砖,也可以一次运送800,你觉得哪种
方式更方便,时间消耗更少?

5、数据预加载

数据预加载策略,顾名思义就是提前把部分要用到的数据,初始化到缓存。如果你在未来某个时间需要用到某个经过复杂计算的数据,才实时去计算的话,可能耗时比较大。这时候,我们可以采取预取思想,提前把可能需要的数据计算好,放到缓存中,等需要的时候,去缓存取就行。这将大幅度提高接口性能。

场景举例:

  • 例如地区数据或者一些数据字典数据,可以在项目启动时预加载到缓存中,在使用时从缓存获取,提升性能;

  • 部分报表类数据,关联业务表很多,实时计算比较耗时,可以通过定时任务,在晚上业务不繁忙时,将数据生成好存放到ElasticSearch中,从Es中查询,提供性能。

项目启动执行方法:

  • 可以通过实现ApplicationRunner接口中的run方法,实现启动时执行。方法执行时,项目已经初始化完毕,是可以正常提供服务

public class DataInitUtil implements ApplicationRunner{
  @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("在项目启动时,会执行这个方法中的代码");
    }
}
  • 使用注解@PostConstruct,需要在项目执行之前执行一些方法,就在目标方法上添加该注解, 存在问题:若执行方法耗时过长,会导致项目在方法执行期间无法提供服务。

  • 实现CommandLineRunner接口 然后在run方法里面调用需要调用的方法即可, 可以通过java -jar demo.jar arg1传参;

  • 实现ApplicationListener接口

@Component
public class ApplicationListenerImpl implements ApplicationListener<ApplicationStartedEvent> {
    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        System.out.println("listener");
    }
}

总结:注解方式@PostConstruct 始终最先执行

  1. 如果监听的是ApplicationStartedEvent 事件,则一定会在CommandLineRunner和ApplicationRunner 之前执行;

  2. 如果监听的是ApplicationReadyEvent 事件,则一定会在CommandLineRunner和ApplicationRunner 之后执行;

  3. CommandLineRunner和ApplicationRunner 默认是ApplicationRunner先执行,如果双方指定了@Order 则按照@Order的大小顺序执行,小的先执行。

4、使用缓存技术

在适当的业务场景,恰当地使用缓存,是可以大大提高接口性能的。缓存其实就是一种空间换时间的思想,就是你把要查的数据,提前放好到缓存里面,需要时,直接查缓存,而避免去查数据库或者计算的过程。

这里的缓存包括:Redis缓存,JVM本地缓存,memcached,或者Map等等。我举个我工作中,一次使用缓存优化的设计吧,比较简单,但是思路很有借鉴的意义。场景举例:

618509e9b88d62607445b9d52ddfb98f.png

这里用户的基本信息包含积分、期望工作地等很多信息,首页需要根据这些信息,个性化展示数据;如果每次都去查询,比较费时,可以考虑将数据缓存到redis,提高性能。

7、池化技术

池化技术最常见的是线程池应用:

  • 如果你每次需要用到线程,都去创建,就会有增加一定的耗时;

  • 线程池可以重复利用线程,避免不必要的耗时;

  • 池化技术不仅仅指线程池,很多场景都有池化思想的体现,它的本质就是预分配与循环使用。


8、事件回调
  • 如果你调用一个系统B的接口,但是它处理业务逻辑,耗时需要10s甚至更多。然后你是一直阻塞等待,直到系统B的下游接口返回,再继续你的下一步操作吗?这样显然不合理。

  • 我们可以采用事件回调机制,即我们不用阻塞等待系统B的接口,而是先去做别的操作。等系统B的接口处理完,通过事件回调通知,我们接口收到通知再进行对应的业务操作即可。如IO多路复用模型实现。


9、串行改为并行调用

假设我们设计一个APP首页的接口,它需要查用户获奖经历、需要查资格证书、需要查岗位信息等等。那你是一个一个接口串行调,还是并行调用呢?

可以使用CompletableFuture 并行调用提高性能,类似也可以使用多线程处理。

// 查询获奖经历
LambdaQueryWrapper<RewardExp> rewardExpQuery = new LambdaQueryWrapper<RewardExp>()
                .eq(RewardExp::getResumeId, resume.getId())
                .eq(RewardExp::getDelFlag, NORMAL)
                .orderByDesc(RewardExp::getDate);
CompletableFuture<List<RewardExp>> rewardExpFuture = CompletableFuture.supplyAsync(() ->
                rewardExpMapper.selectList(rewardExpQuery)
        );


// 查询资格证书
LambdaQueryWrapper<Credential> credentialQuery = new LambdaQueryWrapper<Credential>()
                .eq(Credential::getResumeId, resume.getId())
                .eq(Credential::getDelFlag, NORMAL)
                .orderByDesc(Credential::getDate);
CompletableFuture<List<Credential>> credentialFuture = CompletableFuture.supplyAsync(() ->
                credentialMapper.selectList(credentialQuery)
        );


// 查询岗位信息
LambdaQueryWrapper<ResumeJobs> jobsQuery = new LambdaQueryWrapper<ResumeJobs>()
                .eq(ResumeJobs::getResumeId, resume.getId()).eq(ResumeJobs::getDelFlag, NORMAL);
        CompletableFuture<List<ResumeJobs>> jobsFuture = CompletableFuture.supplyAsync(() ->
                resumeJobsMapper.selectList(jobsQuery)
        );
CompletableFuture.allOf(rewardExpFuture, credentialFuture, jobsFuture).join();

10、锁的粒度和范围控制

在高并发场景,为了防止超卖等情况,我们经常需要加锁来保护共享资源。但是,如果加锁的粒度过粗,是很影响接口性能的。什么是加锁粒度呢?举一个生活中的示例:例如:你在家里上卫生间,你只需要把卫生间锁住,而不用把家里的每个房间都锁住。无论是使用synchronized加锁还是redis分布式锁,只需要在共享临界资源加锁即可,不涉及共享资源的,就不必要加锁。如下示例代码,只是方法A涉及共享资源操作,就只需要锁住A方法就好;很多小伙伴容易一锅端,全锁住,如果锁住的代码耗时,其实是比较影响性能的。错误:

void wrongMethod(){
    synchronized (this) {
       doMethodB();
       A();
    }
}

正确:

void rightMethod(){
    doMethodB();
    synchronized (this) {
       A();
    }
}
11、部分数据暂存文件中

如果接口耗时瓶颈就在数据库插入操作这里,用批量操作等策略,效果还不理想,就可以考虑用文件或者消息队列、redis等暂存。有时候批量数据放到文件,会比插入数据库效率更高。该策略的主要思想:就是在大数据量时,将业务数据写入文件中,再通过异步的方式去消费文件中的数据,执行对应的业务逻辑,减少数据库DB的瞬时压力。

场景举例:

3219dfaa9c107d2777166a6a27058253.png

由于季度考勤数据量很大,逐个接收处理汇总很费时,可以采用将数据按15天分组写入到一个文件中,然后异步去消费文件中数据进行汇总处理。

12、避免长事务问题
  • 长事务在DB服务端的表现是session持续时间长;

  • 期间可能伴随cpu、内存升高,严重者可导致DB服务端整体响应缓慢,导致在线应用无法使用;

  • 所以在线高并发业务中应该尽量避免长事务的发生。产生长事务的原因,除了sql本身可能存在问题外,和应用层的事务控制逻辑也有很大的关系。

如何避免长事务问题:

  1. RPC远程调用不要放到事务里面;

  2. 一些查询相关的操作,尽量放到事务之外;

  3. 事务中避免处理太多数据;

  4. 并发场景下,尽量避免使用@Transactional注解声明式事务粒度太大,使用TransactionTemplate的编程式事务灵活控制事务的范围。

如何解决长事务问题:

  1. 增加对长事务的监控,记录长事务的logId,根据logId能查询到整个请求调用链日志,可以明确是哪个服务的哪个接口的哪个方法产生的;

  2. 根据日志检查是否存在慢SQL;

  3. 检查对应服务是否存在RPC远程调用包裹在事务中;

  4. 检查是否接口中使用了@Transactional注解声明式事务。程序示例:

@Transactional
public int createUser(User user){
    //保存用户信息
    userDao.save(user);
    passCertDao.updateFlag(user.getPassId());
    // 该方法为远程RPC接口
    sendEmailRpc(user.getEmail());
    return user.getUserId();
}

直接使用@Transactional 注解,Spring的声明式事务,整个方法都在事务中,而且里面存在远程RPC调用,容易出现长事务问题。

13、深度分页问题

数据库的深度分页问题,比较影响接口性能,如下所示SQL语句:

select 
    id,name,balance from account 
where 
    create_time> '2020-09-19' 
limit 100000, 10;
limit 100000,10意味着会扫描100010行,丢弃掉前100000行,最后返回10
select id,name,balance FROM account where id > 100000 limit 10;

这样的话,后面无论翻多少页,性能都会不错的,因为命中了id主键索引。但是这种方式有局限性:需要一种类似连续自增的字段,而且需要前端把上次最大值传给后端。

2. 延迟关联法 延迟关联法,就是把条件转移到主键索引树,然后减少回表。优化后的SQL如下:

select
    acct1.id,acct1.name,acct1.balance 
FROM 
    account acct1 
INNER JOIN (
    SELECT 
       a.id 
    FROM 
       account a 
    WHERE 
       a.create_time > '2020-09-19' limit 100000, 10
) AS acct2 
on 
    acct1.id= acct2.id;
优化思路就是,先通过idx_create_time二级索引树查询到满足条件的主键ID,再与原表通过主键ID内连接,这样后面直接走了主键索引了,同时也减少了回表。

14、程序逻辑优化

优化程序逻辑、程序代码,是可以节省耗时的。比如,你的程序创建多不必要的对象、或者程序逻辑混乱,多次重复查数据库、又或者你的实现逻辑算法不是最高效的等等。常见思路:通过使用visiol建模工具或者其他方法,梳理清楚代码逻辑,检查是否存在不必要的对象创建、逻辑调用或者代码细节之类的,是否符合一些编码规范。

15、压缩传输内容

因为文件太大传入会比较消耗网络带宽资源,进一步影响到服务接口的耗时消耗。例如后端需要返回图片到前端加载,该优化策略可以找UI在不影响图片清晰度的前提下,对图片进行适当压缩,节省网络带宽,提高接口性能。

16、考虑分库分表及Nosql

如果确实是因为数据量太大导致的性能问题,可以考虑使用分库分表策略,包括垂直拆分和水平拆分,在程序查询sql中性能得到提升。同时也可以考虑ElasticSearch,对大数据查询都有很好的性能支持。场景举例:之前有个业务需求,需要查询用户报表数据,报表数据包括用户维度的很多属性信息,用户表数据量很大,查询时需要join很多表,如果用关系型数据库存在严重性能问题,如下图所示

55a4105e57d29e2ba23f7be74eb109e8.png

解决方案:通过XxlJob定时任务,在晚上业务不繁忙时,将用户报表数据离线生成好,并存放到ElasticSearch中,界面上展示时后端直接通过Es查询,提高了查询接口性能。

17、线程池设计

我们使用线程池,就是让任务并行处理,更高效地完成任务。但是有时候,如果线程池设计不合理,接口执行效率则不太理想。

一般我们需要关注线程池的这几个参数:核心线程、最大线程数量、阻塞队列。

  • 如果核心线程过小,则达不到很好的并行效果;

  • 如果阻塞队列不合理,不仅仅是阻塞的问题,甚至可能会OOM;

  • 如果线程池不区分业务隔离,有可能核心业务被边缘业务拖垮,如下图:如果不做线程池隔离,在普通接口出问题时,就会影响核心业务接口,出现生产事故。

18afe557c40ba1a284dd02707f362126.png

18、其他问题
  1. 在抽奖活动或者投票活动等高并发场景下,瞬间出现很大流量的场景,会导致线程打满或者DB压力,因此对于高并发场景下,接口一定要接入限流设置,如Guava限流等;

  2. 有时候操作文件流或者其他资源,用完之后记得关闭,如果忘记关闭也会导致资源占用过高,影响性能。

总结

解决服务接口性能问题,是程序员进阶的必经之路。总的来说性能优化通用方法是:从用户发起请求的整个链路分析,将分隔相关环节加上log日志,打印环节耗时,找到接口性能问题出现位置,再结合以上介绍的优化方案进行处理。

作者:美丽的程序人生
链接://juejin.cn/post/7239908637094821949

f5ea1d976d1c1ff984d58e4bf36ff4ea.gif

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
如果您下载了本程序,但是该程序存在问题无法运行,那么您可以选择退款或者寻求我们的帮助(如果找我们帮助的话,是需要追加额外费用的)。另外,您不会使用资源的话(这种情况不支持退款),也可以找我们帮助(需要追加额外费用) 随着移动互联网技术的发展和用户需求的变化,【小程序名称】应运而生,以其轻量化、便捷化的设计理念为用户提供了一种全新的服务模式。作为一款无需下载安装即可使用的应用,【小程序名称】依托于微信庞大的生态系统,让用户在微信内就能轻松实现各种功能操作。 【小程序名称】的核心功能主要集中在【具体服务领域】,例如在线购物、本地生活服务、教育学习或健康管理等。它简化了传统APP繁琐的注册登录流程,支持微信一键授权登录,极大地提升了用户体验。用户通过搜索或扫描二维码,瞬间即可开启使用,享受快速加载、流畅运行的服务。 该小程序界面设计简洁明了,布局合理,易于上手。同时,其特色功能如实时更新的信息推送、个性化推荐以及社交分享功能,让用户能够及时获取所需信息,并方便地将优质内容分享至朋友圈或好友,实现信息的高效传播与互动。 【小程序名称】注重数据安全与隐私保护,严格遵守国家法律法规和微信平台的规定,确保用户数据的安全无虞。此外,其背后的开发团队持续迭代更新,根据用户反馈不断优化产品性能,提升服务质量,致力于打造一个贴近用户需求、充满活力的小程序生态。 总结来说,【小程序名称】凭借其小巧便携、快捷高效的特性,不仅节省了用户的手机存储空间,更为用户提供了无缝衔接的便利服务,是现代生活中不可或缺的一部分,真正实现了“触手可及”的智能生活新体验。只需轻点屏幕,无限精彩尽在掌握之中。
后端开发主要指的是编写那些不能直接看到的代码,这些代码运行在后台并控制着前端的内容。后端开发涉及的内容包括程序设计架构、数据库管理和处理相关的业务逻辑等。简单来说,它负责实现网站的“看不见”的部分,确保数据的存取、平台的稳定性与性能等方面都达到预期效果。 后端开发的主要流程包括: 确定系统的功能和模块:明确系统需要实现哪些功能,以及这些功能应如何被模块化。 技术选型和环境搭建:根据项目需求,选择合适的开发语言和框架,如Java语言和Spring框架。同时,需要搭建开发环境,包括安装IDE、配置开发服务器等。 数据库设计和开发:设计数据库结构,包括表结构和关系设计,使用SQL语言创建表、索引和约束,并编写数据库操作的存储过程或函数。 接口开发:实现与前端或其他系统交互的接口,定义接口的URL、请求方法和参数,并编写接口的处理逻辑。 业务逻辑开发:根据需求和接口设计,实现具体的业务逻辑。 测试:进行单元测试和集成测试,确保代码的正确性和性能。 部署和上线:将代码部署到生产环境或云服务器上,配置和优化服务器,确保系统正常运行。 在后端开发中,技术栈的选择非常重要。常见的后端技术栈包括负载均衡、微服务生态、数据库技术、Spring框架等。这些技术可以帮助后端开发者更好地处理请求、优化系统性能、确保数据的完整性和安全性。 此外,后端开发还涉及一些开发工具的使用,如用于Java应用程序的在线诊断工具Arthas、集成开发环境(IDE)插件Cloud Toolkit、混沌工程故障注入工具ChaosBlade等,这些工具可以帮助开发人员更有效地开发、测试、诊断和部署应用程序。 总的来说,后端开发是网站或应用程序开发中不可或缺的一部分,它确保了数据的存储、处理和交互的安全性、稳定性和效率。随着技术的不断发展,后端开发也在不断进步,为前端提供更好的支持和服务
后端开发主要指的是编写那些不能直接看到的代码,这些代码运行在后台并控制着前端的内容。后端开发涉及的内容包括程序设计架构、数据库管理和处理相关的业务逻辑等。简单来说,它负责实现网站的“看不见”的部分,确保数据的存取、平台的稳定性与性能等方面都达到预期效果。 后端开发的主要流程包括: 确定系统的功能和模块:明确系统需要实现哪些功能,以及这些功能应如何被模块化。 技术选型和环境搭建:根据项目需求,选择合适的开发语言和框架,如Java语言和Spring框架。同时,需要搭建开发环境,包括安装IDE、配置开发服务器等。 数据库设计和开发:设计数据库结构,包括表结构和关系设计,使用SQL语言创建表、索引和约束,并编写数据库操作的存储过程或函数。 接口开发:实现与前端或其他系统交互的接口,定义接口的URL、请求方法和参数,并编写接口的处理逻辑。 业务逻辑开发:根据需求和接口设计,实现具体的业务逻辑。 测试:进行单元测试和集成测试,确保代码的正确性和性能。 部署和上线:将代码部署到生产环境或云服务器上,配置和优化服务器,确保系统正常运行。 在后端开发中,技术栈的选择非常重要。常见的后端技术栈包括负载均衡、微服务生态、数据库技术、Spring框架等。这些技术可以帮助后端开发者更好地处理请求、优化系统性能、确保数据的完整性和安全性。 此外,后端开发还涉及一些开发工具的使用,如用于Java应用程序的在线诊断工具Arthas、集成开发环境(IDE)插件Cloud Toolkit、混沌工程故障注入工具ChaosBlade等,这些工具可以帮助开发人员更有效地开发、测试、诊断和部署应用程序。 总的来说,后端开发是网站或应用程序开发中不可或缺的一部分,它确保了数据的存储、处理和交互的安全性、稳定性和效率。随着技术的不断发展,后端开发也在不断进步,为前端提供更好的支持和服务

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值