接口性能优化技巧

news2025/7/11 19:45:44

背景

我负责的系统在去年初就完成了功能上的建设,然后开始进入到推广阶段。随着推广的逐步深入,收到了很多好评的同时也收到了很多对性能的吐槽。

刚刚收到吐槽的时候,我们的心情是这样的:

当越来越多对性能的吐槽反馈到我们这里的时候,我们意识到,接口性能的问题的优先级必须提高了。

然后我们就跟踪了 1 周的接口性能监控,这个时候我们的心情是这样的:

有 20 多个慢接口,5 个接口响应时间超过 5s,1 个超过 10s,其余的都在 2s 以上,稳定性不足 99.8%。

作为一个优秀的后端程序员,这个数据肯定是不能忍的,我们马上就进入了漫长的接口优化之路。本文就是对我们漫长工作历程的一个总结。

哪些问题会引起接口性能问题

这个问题的答案非常多,需要根据自己的业务场景具体分析。

这里做一个不完全的总结:

  • 数据库慢查询

  • 业务逻辑复杂

  • 线程池设计不合理

  • 锁设计不合理

  • 机器问题(fullGC,机器重启,线程打满)

  • 万金油解决方式

问题解决

| 慢查询(基于 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 以参数的方式传给接口提供方,会有沟通成本(调用方:老子不改!)。

②未加索引

这个是最容易解决的问题,我们可以通过:

show create table xxxx(表名)

   
   

查看某张表的索引。具体加索引的语句网上太多了,不再赘述。不过顺便提一嘴,加索引之前,需要考虑一下这个索引是不是有必要加,如果加索引的字段区分度非常低,那即使加了索引也不会生效。

另外,加索引的 alter 操作,可能引起锁表,执行 sql 的时候一定要在低峰期(血泪史!!!!)

③索引失效

这个是慢查询最不好分析的情况,虽然 mysql 提供了 explain 来评估某个 sql 的查询性能,其中就有使用的索引。

但是为啥索引会失效呢?mysql 却不会告诉咱,需要咱自己分析。大体上,可能引起索引失效的原因有这几个(可能不完全):

需要特别提出的是,关于字段区分性很差的情况,在加索引的时候就应该进行评估。如果区分性很差,这个索引根本就没必要加。

区分性很差是什么意思呢,举几个例子,比如:

  • 某个字段只可能有 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 ...... 1000limit 200

   
   

 
  

当然了,最好是在代码层面做个限制:


   
   
  1. if (ids. size()  >  200) {
  2.     throw new  Exception( "单次查询数据量不能超过200");
  3. }
 
  

⑥单纯的数据量过大

这种问题,单纯代码的修修补补一般就解决不了了,需要变动整个的数据存储架构。或者是对底层 mysql 分表或分库+分表;或者就是直接变更底层数据库,把 mysql 转换成专门为处理大数据设计的数据库。

这种工作是个系统工程,需要严密的调研、方案设计、方案评审、性能评估、开发、测试、联调,同时需要设计严密的数据迁移方案、回滚方案、降级措施、故障处理预案。

除了以上团队内部的工作,还可能有跨系统沟通的工作,毕竟做了重大变更,下游系统的调用接口的方式有可能会需要变化。

出于篇幅的考虑,这个不再展开了,笔者有幸完整参与了一次亿级别数据量的数据库分表工作,对整个过程的复杂性深有体会,后续有机会也会分享出来。

| 业务逻辑复杂

①循环调用

这种情况,一般都循环调用同一段代码,每次循环的逻辑一致,前后不关联。

比如说,我们要初始化一个列表,预置 12 个月的数据给前端:


   
   
  1. List <Model > list  = new ArrayList <>();
  2. for(int i  =  0 ; i  <  12 ; i  + +) {
  3.     Model model  = calOneMonthData(i);  / / 计算某个月的数据,逻辑比较复杂,难以批量计算,效率也无法很高
  4.     list. add(model);
  5. }

这种显然每个月的数据计算相互都是独立的,我们完全可以采用多线程方式进行:


   
   
  1. / / 建立一个线程池,注意要放在外面,不要每次执行代码就建立一个,具体线程池的使用就不展开了
  2. public static ExecutorService commonThreadPool  = new ThreadPoolExecutor( 55300L,
  3.         TimeUnit.SECONDS, new LinkedBlockingQueue <>( 10), commonThreadFactory, new ThreadPoolExecutor.DiscardPolicy());
  4. / / 开始多线程调用
  5. List <Future <Model >> futures = new ArrayList<>();
  6. for(int i  =  0 ; i  <  12 ; i  + +) {
  7.     Future <Model > future  = commonThreadPool.submit(() - > calOneMonthData(i););
  8.     futures. add(future);
  9. }
  10. / / 获取结果
  11. List <Model > list  = new ArrayList <>();
  12. try {
  13.     for (int i  =  0 ; i  < futures. size() ; i  + +) {
  14.       list. add(futures. get(i). get());
  15.    }
  16. } catch ( Exception e) {
  17.    LOGGER. error( "出现错误:", e);
  18. }

②顺序调用

如果不是类似上面循环调用,而是一次次的顺序调用,而且调用之间没有结果上的依赖,那么也可以用多线程的方式进行,例如:

代码上看:


   
   
  1. A a  = do A();
  2. B b  = doB();
  3. C c  = doC(a, b);
  4. D d  = doD(c);
  5. E e  = doE(c);
  6. return doResult(d, e);

那么可用 CompletableFuture 解决:


   
   
  1. CompletableFuture <A > futureA  = CompletableFuture.supplyAsync(() - > do A());
  2. CompletableFuture <B > futureB  = CompletableFuture.supplyAsync(() - > doB());
  3. CompletableFuture.allOf(futureA,futureB)  / / 等a b 两个任务都执行完成
  4. C c  = doC(futureA.join(), futureB.join());
  5. CompletableFuture <D > futureD  = CompletableFuture.supplyAsync(() - > doD(c));
  6. CompletableFuture <E > futureE  = CompletableFuture.supplyAsync(() - > doE(c));
  7. CompletableFuture.allOf(futureD,futureE)  / / 等d e两个任务都执行完成
  8. return doResult(futureD.join(),futureE.join());

这样 A B 两个逻辑可以并行执行,D E 两个逻辑可以并行执行,最大执行时间取决于哪个逻辑更慢。

| 线程池设计不合理

有的时候,即使我们使用了线程池让任务并行处理,接口的执行效率仍然不够快,这种情况可能是怎么回事呢?

这种情况首先应该怀疑是不是线程池设计的不合理。我觉得这里有必要回顾一下线程池的三个重要参数:核心线程数、最大线程数、等待队列。

这三个参数是怎么打配合的呢?当线程池创建的时候,如果不预热线程池,则线程池中线程为 0。当有任务提交到线程池,则开始创建核心线程。

当核心线程全部被占满,如果再有任务到达,则让任务进入等待队列开始等待。

如果队列也被占满,则开始创建非核心线程运行。

如果线程总数达到最大线程数,还是有任务到达,则开始根据线程池抛弃规则开始抛弃。

那么这个运行原理与接口运行时间有什么关系呢?

  • 核心线程设置过小:核心线程设置过小则没有达到并行的效果

  • 线程池公用,别的业务的任务执行时间太长,占用了核心线程,另一个业务的任务到达就直接进入了等待队列

  • 任务太多,以至于占满了线程池,大量任务在队列中等待

在排查的时候,只要找到了问题出现的原因,那么解决方式也就清楚了,无非就是调整线程池参数,按照业务拆分线程池等等。

| 锁设计不合理

锁设计不合理一般有两种:锁类型使用不合理 or 锁过粗。

锁类型使用不合理的典型场景就是读写锁。也就是说,读是可以共享的,但是读的时候不能对共享变量写;而在写的时候,读写都不能进行。

在可以加读写锁的时候,如果我们加成了互斥锁,那么在读远远多于写的场景下,效率会极大降低。

锁过粗则是另一种常见的锁设计不合理的情况,如果我们把锁包裹的范围过大,则加锁时间会过长,例如:


   
   
  1. public  synchronized  void  doSome () {
  2.      File  f  = calData();
  3.     uploadToS3(f);
  4.     sendSuccessMessage();
  5. }

这块逻辑一共处理了三部分,计算、上传结果、发送消息。显然上传结果和发送消息是完全可以不加锁的,因为这个跟共享变量根本不沾边。

因此完全可以改成:


   
   
  1. public  void  doSome( ) {
  2.      File f =  null;
  3.      synchronized( this) {
  4.         f =  calData();
  5.     }
  6.      uploadToS3(f);
  7.      sendSuccessMessage();
  8. }

| 机器问题(fullGC,机器重启,线程打满)

造成这个问题的原因非常多,笔者就遇到了定时任务过大引起 fullGC,代码存在线程泄露引起 RSS 内存占用过高进而引起机器重启等待诸多原因。

需要结合各种监控和具体场景具体分析,进而进行大事务拆分、重新规划线程池等等工作。

| 万金油解决方式

万金油这个形容词是从我们单位某位老师那里学来的,但是笔者觉得非常贴切。这些万金油解决方式往往能解决大部分的接口缓慢的问题,而且也往往是我们解决接口效率问题的最终解决方案。

当我们实在是没有办法排查出问题,或者实在是没有优化空间的时候,可以尝试这种万金油的方式。

①缓存

缓存是一种空间换取时间的解决方案,是在高性能存储介质上(例如:内存、SSD 硬盘等)存储一份数据备份。

当有请求打到服务器的时候,优先从缓存中读取数据。如果读取不到,则再从硬盘或通过网络获取数据。

由于内存或 SSD 相比硬盘或网络 IO 的效率高很多,则接口响应速度会变快非常多。缓存适合于应用在数据读远远大于数据写,且数据变化不频繁的场景中。

从技术选型上看,有这些:

  • 简单的 map

  • guava 等本地缓存工具包

  • 缓存中间件:redis、tair 或 memcached

当然,memcached 现在用的很少了,因为相比于 redis 他不占优势。tair 则是阿里开发的一个分布式缓存中间件,他的优势是理论上可以在不停服的情况下,动态扩展存储容量,适用于大数据量缓存存储。

相比于单机 redis 缓存当然有优势,而他与可扩展 Redis 集群的对比则需要进一步调研。

进一步的,当前缓存的模型一般都是 key-value 模型。如何设计 key 以提高缓存的命中率是个大学问,好的 key 设计和坏的 key 设计所提升的性能差别非常大。

而且,key 设计是没有一定之规的,需要结合具体的业务场景去分析。各个大公司分享出来的相关文章,缓存设计基本上是最大篇幅。

②回调 or 反查

这种方式往往是业务上的解决方式,在订单或者付款系统中应用的比较多。

举个例子:当我们付款的时候,需要调用一个专门的付款系统接口,该系统经过一系列验证、存储工作后还要调用银行接口以执行付款。

由于付款这个动作要求十分严谨,银行侧接口执行可能比较缓慢,进而拖累整个付款接口性能。

这个时候我们就可以采用 fast success 的方式:当必要的校验和存储完成后,立即返回 success,同时告诉调用方一个中间态“付款中”。

而后调用银行接口,当获得支付结果后再调用上游系统的回调接口返回付款的最终结果“成果”or“失败”。这样就可以异步执行付款过程,提升付款接口效率。

当然,为了防止多业务方接入的时候回调接口不统一,可以把结果抛进 kafka,让调用方监听自己的结果。

总结

本文是笔者对工作中遇到的性能优化问题的一个简单的总结,可能有不完备的地方,欢迎大家讨论交流。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/733816.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

阿里云服务器架构X86_GPU_裸金属_超级计算集群详解

阿里云服务器架构有什么区别&#xff1f;X86计算、ARM计算、GPU/FPGA/ASIC、弹性裸金属服务器、超级计算集群有什么区别&#xff1f;阿里云服务器网分享云服务器ECS架构详细说明&#xff1a; 目录 阿里云服务器ECS架构说明 X86计算 ARM计算 GPU/FPGA/ASIC 弹性裸金属服务…

基于simulink使用SAD方法进行运动检测(附源码)

一、前言 此示例演示如何使用绝对差分总和 &#xff08;SAD&#xff09; 方法来检测视频序列中的运动。此示例将 SAD 独立应用于视频序列的四个象限。如果在象限中检测到运动&#xff0c;则示例会以红色突出显示该象限。 二、模型 下图显示了移动侦测示例模型&#xff1a; 三…

一文搞懂Java线程池执行原理核心参数

文章目录 前言一、为什么要使用线程池创建线程&#xff1f;二、线程池的核心参数&#xff08;重点&#xff09;1.核心线程数2.最大线程数3.救急线程的存活时间4.救急线程的时间单位5.任务队列6.线程工厂7.任务拒绝策略 三、线程池的执行原理四、一个小案例总结 前言 最近面试过…

CEC2018动态多目标优化算法:基于自适应启动策略的混合交叉动态约束多目标优化算法(MC-DCMOEA)求解CEC2018

一、动态多目标优化问题 1.1问题定义 1.2 动态支配关系定义 二、 基于自适应启动策略的混合交叉动态多目标优化算法 基于自适应启动策略的混合交叉动态多目标优化算法&#xff08;Mixture Crossover Dynamic Constrained Multi-objective Evolutionary Algorithm Based on Se…

Blender基础入门(1):Blender建模系统简单介绍

文章目录 我个人的Blender专栏前言偏好设置推荐常用组合按键空格&#xff08;需在偏好设置里面选择空格->工具&#xff09;ShiftA&#xff08;添加物体&#xff09;右键&#xff1a;物体对象操作I:显示关键帧操作~&#xff08;1左边的波浪号&#xff09;&#xff1a;视角选择…

【算法与数据结构】1047、LeetCode删除字符串中的所有相邻重复项

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;这道题和【算法与数据结构】20、LeetCode有效的括号类似&#xff0c;本质上都是找一个字符的匹配字符。…

深入理解Linux网络——内核是如何接收到网络包的

文章目录 一、相关实际问题二、数据是如何从网卡到协议栈的1、Linux网络收包总览2、Linux启动1&#xff09;创建ksotfirqd内核线程2&#xff09;网络子系统初始化3&#xff09;协议栈注册4&#xff09;网卡驱动初始化5&#xff09;网卡启动 3、迎接数据的到来1&#xff09;硬中…

Python潮流周刊#10:Twitter 的强敌 Threads 是用 Python 开发的!

△点击上方“Python猫”关注 &#xff0c;回复“1”领取电子书 你好&#xff0c;我是猫哥。这里每周分享优质的 Python 及通用技术内容&#xff0c;大部分为英文&#xff0c;已在小标题注明。&#xff08;标题取自其中一则分享&#xff0c;不代表全部内容都是该主题&#xff0c…

Vue+elementUI实现下拉框多选和反选

Vue代码如下&#xff1a; <el-form-item label"下拉框名称&#xff1a;"><el-select size"mini" v-model"testModelName" focus"getSelectInfo" :disabled"SelectStyle" filterable clearable placeholder"&…

SpringBoot实战(二十)集成Druid连接池

目录 一、简介1.定义2.特点3.竞品对比 二、搭建测试项目1.Maven依赖2.yaml配置2.1 JDBC配置2.2 连接池配置2.3 监控配置 三、测试1.查看监控页面2.单元测试 四、补充&#xff1a;1.如何打印慢SQL&#xff1f;2.去除广告3.如何手动获取监控内容 一、简介 1.定义 Druid数据库连…

简析电力系统网络靶场建设的价值、挑战与趋势

在当下已经演变为持久战的俄乌地区冲突中&#xff0c;通信、交通、能源供应等相关国家关键基础设施一直是双方互相攻击的重点目标。同时&#xff0c;“网络战”作为先行战场&#xff0c;也把关基设施作为主阵地&#xff0c;不断以相对轻量级成本制造比想象中更广泛的破坏和社会…

Haproxy搭建Web群集和脑裂的概念

目录 脑裂概念 脑裂如何生的: 解决方法 Haproxy概念 HAProxy的主要特性有&#xff1a; HAProxy负载均衡策略非常多&#xff0c;常见的有如下8种&#xff1a; CDN Nginx LVS haproxy haproxy服务器部署 关闭防火墙 编译安装 Haproxy Haproxy服务器配置 添加haprox…

Spring MVC教程

Spring MVC属于SpringFrameWork的后续产品&#xff0c;已经融合在Spring Web Flow里面。 Spring 框架提供了构建 Web 应用程序的全功能 MVC 模块。 使用 Spring 可插入的 MVC 架构&#xff0c;从而在使用Spring进行WEB开发时&#xff0c;可以选择使用Spring的Spring MVC框架或集…

明明都是2000坐标的地形图,怎么位置就不一样呢?

前几天有一个同事在工作中遇到一个问题,核心说起来就是,甲方发来两个年份的同一个片区的地形图,并且言之凿凿都是2000坐标的,但是在一个CAD复制后,到另一个CAD中使用“粘贴到原坐标”,就是位置对不上。 以下使用测试数据还原一下情景。 地形图A,是一个高程点: 地形图…

【网络系统集成】Windows Server集群实验

1.实验名称 Windows Server集群实验 2.实验目的 通过使用Windows 2003进行实验,理解与掌握服务器技术与系统集成相关知识点。 3.实验内容 (1)在Vmware Workstation中安装Windows Server 2003 (2)在Windows Server 2008中完成DNS,WEB的配置

LinuxI2C应用编程——访问EEPROM

文章目录 介绍读芯片手册代码编译运行 阅读博文&#xff1a;LinuxI2C应用编程——I2C-Tools的使用 介绍 EEPROM (Electrically Erasable Programmable read only memory)&#xff0c;指带电可擦可编程只读存储器。是一种掉电后数据不丢失的存储芯片。 读芯片手册 首先按如图…

SSMP整合案例(15) 解决分页中删除最后一页的最后一条数据,导致查询错位问题

上文 我们还是做了个比较重要的是 将我们的查询全部逻辑改为了分页查询 但是 目前 我们的删除和分页配合起来 它会有一点点问题 例如 这种情况 我们最后一页只有一条数据了 我们操作删除 将这条数据给他干掉 删除完之后 它会调分页查询 但我们当前页的条件还是之前的 例如 我…

【网络】UDP协议详解

目录 UDP的感性理解 UDP协议格式 UDP协议格式感性理解 UDP特点 UDP的缓冲区 UDP的感性理解 UDP的传输过程类似于寄信&#xff0c;假设你要写一封家书寄回家里&#xff1a;首先你要在信封上填写好寄件人和收件人的地址&#xff0c;其次在贴好邮票&#xff0c;最后将信件投放…

MySQL数据库 库表操作

1. (1) mysql> create database Market; 创建数据库 mysql> use Market 使用Market数据库(2) mysql> create table customers(-> c_num int(11) primary key auto_increment,-> c_name varchar(50),-> c_contact varchar(50),-> c_city varc…

ChatGPT炒股:从巨潮资讯网上批量下载特定主题的股票公告

巨潮资讯网是股票公告的指定披露渠道之一&#xff0c;上面有非常详细的A股股票公告内容。 现在&#xff0c;我们要获取2023-01-04~2023-07-04期间所有新三板公司中标题包含“2023年日常性关联交易”的公告。 首先从network中获取到真实网址&#xff1a;http://www.cninfo.com…