我的编程空间,编程开发者的网络收藏夹
学习永远不晚

java接口性能优化技巧

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

java接口性能优化技巧

背景

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

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

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

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

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

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

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能。

项目地址:github.com/YunaiV/ruoy…

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

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

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

  • 数据库慢查询
  • 业务逻辑复杂
  • 线程池设计不合理
  • 锁设计不合理
  • 机器问题(fullGC,机器重启,线程打满)
  • 万金油解决方式

基于微服务的思想,构建在 B2C 电商场景下的项目实战。核心技术栈,是 Spring Boot + Dubbo 。未来,会重构成 Spring Cloud Alibaba 。

项目地址:github.com/YunaiV/onem…

问题解决

慢查询(基于 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 ...... 1000) limit 200
//当然了,最好是在代码层面做个限制:
if (ids.size() > 200) {
    throw new Exception("单次查询数据量不能超过200");
}

⑥单纯的数据量过大

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

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

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

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

业务逻辑复杂

①循环调用

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

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

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

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

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

②顺序调用

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

代码上看:

A a = doA();
B b = doB();
C c = doC(a, b);
D d = doD(c);
E e = doE(c);
return doResult(d, e);

那么可用 CompletableFuture 解决:

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

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

线程池设计不合理

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

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

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

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

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

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

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

  • 核心线程设置过小:核心线程设置过小则没有达到并行的效果
  • 线程池公用,别的业务的任务执行时间太长,占用了核心线程,另一个业务的任务到达就直接进入了等待队列
  • 任务太多,以至于占满了线程池,大量任务在队列中等待

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

锁设计不合理

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

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

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

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

public synchronized void doSome() {
    File f = calData();
    uploadToS3(f);
    sendSuccessMessage();
}

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

因此完全可以改成:

public void doSome() {
    File f = null;
    synchronized(this) {
        f = calData();
    }
    uploadToS3(f);
    sendSuccessMessage();
}

机器问题(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,让调用方监听自己的结果。

以上就是java接口性能优化技巧的详细内容,更多关于java接口性能优化的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

java接口性能优化技巧

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

PHP接口性能优化之内存管理技巧(PHP接口中内存管理的性能优化技巧)

PHP接口性能优化之内存管理技巧在PHP接口中优化内存管理对于提高性能至关重要。以下技巧可帮助优化:对象池:预分配并重复使用对象,避免创建和销毁。延迟加载:仅在需要时加载对象,减少内存占用。惰性求值:推迟计算,只在需要时使用内存。缓存:存储经常访问的数据,减少数据库查询。内存泄漏检测:检测并修复不再需要但仍被引用的对象。轻量级数据结构:使用哈希表和链表等轻量级数据结构存储简单数据。减少全局变量:尽量使用局部变量或依赖注入。释放未使用的内存:使用unset()函数或引用循环释放不再需要的对象。优化数据库查询
PHP接口性能优化之内存管理技巧(PHP接口中内存管理的性能优化技巧)
2024-04-02

有哪些Java性能优化技巧

这篇文章主要讲解了“有哪些Java性能优化技巧”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“有哪些Java性能优化技巧”吧!  1.在你确认必要之前不要优化  你应该遵循常见的最佳实践做法并
2023-06-02

java接口性能如何优化

优化Java接口的性能可以从以下几个方面入手:1. 减少接口方法数量:接口方法越多,调用时的开销就越大。因此,可以考虑将一些耗时较长的方法进行合并或者拆分为多个小的方法。2. 接口设计合理化:合理的接口设计可以减少不必要的方法调用,从而提高
2023-08-20

Java性能的优化技巧有哪些

这篇文章主要讲解了“Java性能的优化技巧有哪些”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Java性能的优化技巧有哪些”吧!1.对象的生成和大小的调整。 JAVA程序设计中一个普遍的问题
2023-06-17

Node.js OAuth:性能优化技巧

Node.js OAuth 性能优化技巧对于优化 Node.js 应用性能至关重要。通过实施这些技巧,开发人员可以提高 OAuth 请求的效率,从而缩短响应时间和改善用户体验。
Node.js OAuth:性能优化技巧
2024-02-19

PHP 性能优化:内存优化技巧

内存管理对 php 性能优化至关重要。优化内存使用可以通过以下技巧实现:使用高效的数据结构,如数组而非链表。避免不必要的内存拷贝,使用引用 (&) 或赋值 (=) 进行变量传递。监控内存使用情况,使用 xdebug 等工具检测泄漏。优化字符
PHP 性能优化:内存优化技巧
2024-05-10

Java接口异步调用优化技巧详解

本文详细介绍了在Java开发中,如何通过异步调用等技巧来优化接口的性能,有效避免阻塞和提高并发处理能力,提升系统的稳定性和响应速度
2023-05-19

利用PHP扩展提升接口性能的技巧(如何通过PHP扩展优化接口性能?)

利用PHP扩展提升接口性能:通过OPCache、Redis、Swoole和Xdebug等扩展,可有效优化PHP接口性能。OPCache编译脚本字节码,提高加载速度。Redis缓存数据,减少数据库查询次数。Swoole提供异步I/O和并发功能,提高吞吐量。Xdebug分析脚本性能,识别瓶颈。最佳实践包括启用OPCache、使用Redis缓存、利用Swoole协程,优化查询和禁用调试模式,以打造响应迅速的PHP接口。
利用PHP扩展提升接口性能的技巧(如何通过PHP扩展优化接口性能?)
2024-04-02

PHP 函数性能优化技巧

php函数性能优化技巧包括:避免不必要的对象创建;减少函数调用次数;使用函数缓存;优化数据库查询;使用第三方工具分析和优化性能。PHP 函数性能优化技巧PHP 函数的性能优化至关重要,因为它可以大幅提升应用程序的响应速度。本文将介绍几个实
PHP 函数性能优化技巧
2024-04-12

Python 性能优化技巧总结

1.使用测量工具,量化性能才能改进性能,常用的timeit和memory_profiler,此外还有profile、cProfile、hotshot等,memory_profiler用了psutil,所以不能跟踪cpython的扩展; 2.
2022-06-04

Python 代码性能优化技巧

1. Vim2. Eclipse with PyDev3. Sublime Text4. Emacs5. Komodo Edit6. PyCharm7. Wing8. PyScripter9. The Eric Python IDE10. 
2023-01-31

Golang GraphQL API性能优化技巧

使用缓存、批处理、协程和数据库索引可以优化 graphql api 的性能,提升响应能力和吞吐量,有效避免应用程序在高负载下遇到的性能问题。Go GraphQL API 性能优化技巧前言GraphQL 是一种强大的查询语言,可用于创建灵
Golang GraphQL API性能优化技巧
2024-05-07

Vista系统性能优化技巧

解决Vista狂读硬盘现象: a. 取消磁盘碎片整理 b. 关闭Windows Defender c. 关闭系统还原 Vista常见问题整理: 1. 如何关闭UAC? 控制面板→用户帐户&ra编程客栈rr;打开或关闭用户账户控制&
2023-05-23

MySQL性能优化技巧分享

MySQL性能优化在互联网公司MySQL的使用非常广泛,大家经常会有MySQL性能优化方面的需求。整理了一些在MySQL优化方面的实用技巧。Schema与数据类型优化整数通常是标识列最好的选择,因为它们很快并且可以使用AUTO_INCREM
2022-05-20

golang函数性能优化技巧

可以通过以下技巧优化 go 函数性能:使用缓存以避免重复计算。使用 goroutine 并发化计算以提高效率。对于关键计算使用汇编代码以提升性能。选择适当的数据结构,如 slice、map 和 channel,优化数据存储和检索。避免不必要
golang函数性能优化技巧
2024-04-27

PHP代码性能优化的技巧

本篇内容主要讲解“PHP代码性能优化的技巧”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“PHP代码性能优化的技巧”吧!PHP代码性能优化1. 不要随便就复制变量有时候为了使 PHP 代码更加整洁
2023-06-17

python性能优化技巧有哪些

小编给大家分享一下python性能优化技巧有哪些,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!性能优化技巧1. 限制CPU和内存使用量如果Python程序占用资源
2023-06-27

优化MySQL视图的性能技巧

标题:优化MySQL视图的性能技巧MySQL视图是一种虚拟的表,它是一个基于查询结果的表。在实际开发中,我们经常会使用视图来简化复杂的查询操作,提高代码的可读性和维护性。然而,当数据量较大或者视图的复杂度较高时,视图的性能可能会受到影响。
优化MySQL视图的性能技巧
2024-03-15

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录