JAVA代码优化,接口优化,SQL优化 (小技巧)(四)

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: JAVA代码优化,接口优化,SQL优化 (小技巧)(四)

🟡第二章: 接口性能优化


接口性能优化对于从事后端开发的同学来说,肯定再熟悉不过了,因为它是一个跟开发语言无关的公共问题。


该问题说简单也简单,说复杂也复杂。


有时候,只需加个索引就能解决问题。


有时候,需要做代码重构。


有时候,需要增加缓存。


有时候,需要引入一些中间件,比如mq。


有时候,需要需要分库分表。


有时候,需要拆分服务。


等;


导致接口性能问题的原因千奇百怪,不同的项目不同的接口,原因可能也不一样。


本文我总结了一些行之有效的,优化接口性能的办法,给有需要的朋友一个参考。


1.索引

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


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


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


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


该sql语句加索引了没?

加的索引生效了没?

mysql选错索引了没?

最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。


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走某个索引。


至于为什么mysql会选错索引,后面有专门的文章介绍的,这里先留点悬念。


2. sql优化

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


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


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


由于这些技巧在我之前的文章中已经详细介绍过了,在这里我就不深入了。


3. 远程调用

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


比如有这样的业务场景:


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


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


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


调用过程如下图所示:


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


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


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


3.1 并行调用

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

如下图所示:


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


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


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

public UserInfo getUserInfo(Long id) throws InterruptedException, ExecutionException {
    final UserInfo userInfo = new UserInfo();
    CompletableFuture userFuture = CompletableFuture.supplyAsync(() -> {
        getRemoteUserAndFill(id, userInfo);
        return Boolean.TRUE;
    }, executor);
    CompletableFuture bonusFuture = CompletableFuture.supplyAsync(() -> {
        getRemoteBonusAndFill(id, userInfo);
        return Boolean.TRUE;
    }, executor);
    CompletableFuture growthFuture = CompletableFuture.supplyAsync(() -> {
        getRemoteGrowthAndFill(id, userInfo);
        return Boolean.TRUE;
    }, executor);
    CompletableFuture.allOf(userFuture, bonusFuture, growthFuture).join();
    userFuture.get();
    bonusFuture.get();
    growthFuture.get();
    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以内。


4.2 死循环

有些小伙伴看到这个标题,可能会感到有点意外,死循环也算?


代码中不是应该避免死循环吗?为啥还是会产生死循环?


有时候死循环是我们自己写的,例如下面这段代码:

while(true) {
    if(condition) {
        break;
    }
    System.out.println("do samething");
}


这里使用了while(true)的循环调用,这种写法在CAS自旋锁中使用比较多。


当满足condition等于true的时候,则自动退出该循环。


如果condition条件非常复杂,一旦出现判断不正确,或者少写了一些逻辑判断,就可能在某些场景下出现死循环的问题。


出现死循环,大概率是开发人员人为的bug导致的,不过这种情况很容易被测出来。


还有一种隐藏的比较深的死循环,是由于代码写的不太严谨导致的。如果用正常数据,可能测不出问题,但一旦出现异常数据,就会立即出现死循环。


4.3 无限递归

如果想要打印某个分类的所有父分类,可以用类似这样的递归方法实现:


public void printCategory(Category category) {
  if(category == null 
      || category.getParentId() == null) {
     return;
  } 
  System.out.println("父分类名称:"+ category.getName());
  Category parent = categoryMapper.getCategoryById(category.getParentId());
  printCategory(parent);
}


正常情况下,这段代码是没有问题的。


但如果某次有人误操作,把某个分类的parentId指向了它自己,这样就会出现无限递归的情况。导致接口一直不能返回数据,最终会发生堆栈溢出。


建议写递归方法时,设定一个递归的深度,比如:分类最大等级有4级,则深度可以设置为4。然后在递归方法中做判断,如果深度大于4时,则自动返回,这样就能避免无限循环的情况。


5. 异步处理

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


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


接口内部流程图如下:


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


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


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


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


5.1 线程池

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

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


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


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


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


5.2 mq

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



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


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


6. 避免大事务

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


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


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

下面用一张图看看大事务引发的问题。




从图中能够看出,大事务问题可能会造成接口超时,对接口的性能有直接的影响。


我们该如何优化大事务呢?


少用@Transactional注解

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

事务中避免远程调用

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

有些功能可以非事务执行

有些功能可以异步处理

关于大事务问题我的另一篇文章《让人头痛的大事务问题到底要如何解决?》,它里面做了非常详细的介绍,如果大家感兴趣可以看看。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
14天前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
|
3天前
|
监控 算法 Java
Java虚拟机垃圾回收机制深度剖析与优化策略####
【10月更文挑战第21天】 本文旨在深入探讨Java虚拟机(JVM)中的垃圾回收机制,揭示其工作原理、常见算法及参数调优技巧。通过案例分析,展示如何根据应用特性调整GC策略,以提升Java应用的性能和稳定性,为开发者提供实战中的优化指南。 ####
20 5
|
5天前
|
Java
在Java中,接口之间可以继承吗?
接口继承是一种重要的机制,它允许一个接口从另一个或多个接口继承方法和常量。
23 1
|
16天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
36 6
|
15天前
|
Java
java线程接口
Thread的构造方法创建对象的时候传入了Runnable接口的对象 ,Runnable接口对象重写run方法相当于指定线程任务,创建线程的时候绑定了该线程对象要干的任务。 Runnable的对象称之为:线程任务对象 不是线程对象 必须要交给Thread线程对象。 通过Thread的构造方法, 就可以把任务对象Runnable,绑定到Thread对象中, 将来执行start方法,就会自动执行Runable实现类对象中的run里面的内容。
32 1
|
21天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
44 4
|
27天前
|
安全 Java
在 Java 中使用实现 Runnable 接口的方式创建线程
【10月更文挑战第22天】通过以上内容的介绍,相信你已经对在 Java 中如何使用实现 Runnable 接口的方式创建线程有了更深入的了解。在实际应用中,需要根据具体的需求和场景,合理选择线程创建方式,并注意线程安全、同步、通信等相关问题,以确保程序的正确性和稳定性。
|
26天前
|
Java 数据库连接 数据库
优化之路:Java连接池技术助力数据库性能飞跃
在Java应用开发中,数据库操作常成为性能瓶颈。频繁的数据库连接建立和断开增加了系统开销,导致性能下降。本文通过问题解答形式,深入探讨Java连接池技术如何通过复用数据库连接,显著减少连接开销,提升系统性能。文章详细介绍了连接池的优势、选择标准、使用方法及优化策略,帮助开发者实现数据库性能的飞跃。
27 4
|
24天前
|
存储 Java 开发者
成功优化!Java 基础 Docker 镜像从 674MB 缩减到 58MB 的经验分享
本文分享了如何通过 jlink 和 jdeps 工具将 Java 基础 Docker 镜像从 674MB 优化至 58MB 的经验。首先介绍了选择合适的基础镜像的重要性,然后详细讲解了使用 jlink 构建自定义 JRE 镜像的方法,并通过 jdeps 自动化模块依赖分析,最终实现了镜像的大幅缩减。此外,文章还提供了实用的 .dockerignore 文件技巧和选择安全、兼容的基础镜像的建议,帮助开发者提升镜像优化的效果。
|
25天前
|
Java
Java基础(13)抽象类、接口
本文介绍了Java面向对象编程中的抽象类和接口两个核心概念。抽象类不能被实例化,通常用于定义子类的通用方法和属性;接口则是完全抽象的类,允许声明一组方法但不实现它们。文章通过代码示例详细解析了抽象类和接口的定义及实现,并讨论了它们的区别和使用场景。

热门文章

最新文章

下一篇
无影云桌面