| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> Java知识库 -> 聊聊接口性能优化的11个小技巧 -> 正文阅读 |
|
[Java知识库]聊聊接口性能优化的11个小技巧 |
前言接口性能优化对于从事后端开发的同学来说,肯定再熟悉不过了,因为它是一个跟开发语言无关的公共问题。 该问题说简单也简单,说复杂也复杂。 有时候,只需加个索引就能解决问题。 有时候,需要做代码重构。 有时候,需要增加缓存。 有时候,需要引入一些中间件,比如mq。 有时候,需要需要分库分表。 有时候,需要拆分服务。 等等。。。 导致接口性能问题的原因千奇百怪,不同的项目不同的接口,原因可能也不一样。 本文我总结了一些行之有效的,优化接口性能的办法,给有需要的朋友一个参考。 1.索引接口性能优化大家第一个想到的可能是: 没错,优化索引的成本是最小的。 你通过查看线上日志或者监控报告,查到某个接口用到的某条sql语句耗时比较长。 这时你可能会有下面这些疑问:
最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。 1.1 没加索引sql语句中 项目刚开始的时候,由于表中的数据量小,加不加索引sql查询性能差别不大。 后来,随着业务的发展,表中数据量越来越多,就不得不加索引了。 可以通过命令:
能单独查看某张表的索引情况。 也可以通过命令:
查看整张表的建表语句,里面同样会显示索引情况。 通过
也可以通过
不过这里有一个需要注意的地方是:想通过命令修改索引,是不行的。 目前在mysql中如果想要修改索引,只能先删除索引,再重新添加新的。 删除索引可以用
用
1.2 索引没生效通过上面的命令我们已经能够确认索引是有的,但它生效了没?此时你内心或许会冒出这样一个疑问。 那么,如何查看索引有没有生效呢? 答:可以使用 例如:
结果: 说实话,sql语句没有走索引,排除没有建索引之外,最大的可能性是索引失效了。 下面说说索引失效的常见原因: 1.3 选错索引此外,你有没有遇到过这样一种情况:明明是同一条sql,只有入参不同而已。有的时候走的索引a,有的时候却走的索引b? 没错,有时候mysql会选错索引。 必要时可以使用 至于为什么mysql会选错索引,后面有专门的文章介绍的,这里先留点悬念。 2. sql优化如果优化了索引之后,也没啥效果。 接下来试着优化一下sql语句,因为它的改造成本相对于java代码来说也要小得多。 下面给大家列举了sql优化的15个小技巧: 更详细的内容,可以看我的另一篇文章《聊聊sql优化的15个小技巧》,相信看完你会有很多收获。 3. 远程调用很多时候,我们需要在某个接口中,调用其他服务的接口。 比如有这样的业务场景: 在用户信息查询接口中需要返回:用户名称、性别、等级、头像、积分、成长值等信息。 而用户名称、性别、等级、头像在用户服务中,积分在积分服务中,成长值在成长值服务中。为了汇总这些数据统一返回,需要另外提供一个对外接口服务。 于是,用户信息查询接口需要调用用户查询接口、积分查询接口 和 成长值查询接口,然后汇总数据统一返回。 调用过程如下图所示: 显然这种串行调用远程接口性能是非常不好的,调用远程接口总的耗时为所有的远程接口耗时之和。 那么如何优化远程接口性能呢? 3.1 并行调用上面说到,既然串行调用多个远程接口性能很差,为什么不改成并行呢? 如下图所示: 在java8之前可以通过实现 java8以后通过
3.2 数据异构上面说到的用户信息查询接口需要调用用户查询接口、积分查询接口 和 成长值查询接口,然后汇总数据统一返回。 那么,我们能不能把数据冗余一下,把用户信息、积分和成长值的数据统一存储到一个地方,比如:redis,存的数据结构就是用户信息查询接口所需要的内容。然后通过用户id,直接从redis中查询数据出来,不就OK了? 如果在高并发的场景下,为了提升接口性能,远程接口调用大概率会被去掉,而改成保存冗余数据的数据异构方案。
用户信息、积分和成长值有更新的话,大部分情况下,会先更新到数据库,然后同步到redis。但这种跨库的操作,可能会导致两边数据不一致的情况产生。 4. 重复调用
不信,我们一起看看。 4.1 循环查数据库有时候,我们需要从指定的用户集合中,查询出有哪些是在数据库中已经存在的。 实现代码可以这样写:
这里如果有50个用户,则需要循环50次,去查询数据库。我们都知道,每查询一次数据库,就是一次远程调用。 如果查询50次数据库,就有50次远程调用,这是非常耗时的操作。 那么,我们如何优化呢? 具体代码如下:
提供一个根据用户id集合批量查询用户的接口,只远程调用一次,就能查询出所有的数据。
4.2 死循环有些小伙伴看到这个标题,可能会感到有点意外,死循环也算? 代码中不是应该避免死循环吗?为啥还是会产生死循环? 有时候死循环是我们自己写的,例如下面这段代码:
这里使用了while(true)的循环调用,这种写法在 当满足condition等于true的时候,则自动退出该循环。 如果condition条件非常复杂,一旦出现判断不正确,或者少写了一些逻辑判断,就可能在某些场景下出现死循环的问题。 出现死循环,大概率是开发人员人为的bug导致的,不过这种情况很容易被测出来。
4.3 无限递归如果想要打印某个分类的所有父分类,可以用类似这样的递归方法实现:
正常情况下,这段代码是没有问题的。 但如果某次有人误操作,把某个分类的parentId指向了它自己,这样就会出现无限递归的情况。导致接口一直不能返回数据,最终会发生堆栈溢出。
5. 异步处理有时候,我们接口性能优化,需要重新梳理一下业务逻辑,看看是否有设计上不太合理的地方。 比如有个用户请求接口中,需要做业务操作,发站内通知,和记录操作日志。为了实现起来比较方便,通常我们会将这些逻辑放在接口中同步执行,势必会对接口性能造成一定的影响。 接口内部流程图如下:
上面这个例子中,发站内通知和用户操作日志功能,对实时性要求不高,即使晚点写库,用户无非是晚点收到站内通知,或者运营晚点看到用户操作日志,对业务影响不大,所以完全可以异步处理。 通常异步主要有两种: 5.1 线程池使用 这样接口中重点关注的是业务操作,把其他的逻辑交给线程异步执行,这样改造之后,让接口性能瞬间提升了。 但使用线程池有个小问题就是:如果服务器重启了,或者是需要被执行的功能出现异常了,无法重试,会丢数据。 那么这个问题该怎么办呢? 5.2 mq使用 这样改造之后,接口性能同样提升了,因为发送mq消息速度是很快的,我们只需关注业务操作的代码即可。 6. 避免大事务很多小伙伴在使用spring框架开发项目时,为了方便,喜欢使用 没错,使用@Transactional注解这种声明式事务的方式提供事务功能,确实能少写很多代码,提升开发效率。 但也容易造成大事务,引发其他的问题。 下面用一张图看看大事务引发的问题。 我们该如何优化大事务呢?
关于大事务问题我的另一篇文章《让人头痛的大事务问题到底要如何解决?》,它里面做了非常详细的介绍,如果大家感兴趣可以看看。 7. 锁粒度在某些业务场景中,为了防止多个线程并发修改某个共享数据,造成数据异常。 为了解决并发场景下,多个线程同时修改数据,造成数据不一致的情况。通常情况下,我们会: 但如果锁加得不好,导致锁的粒度太粗,也会非常影响接口性能。 7.1 synchronized在java中提供了 通常有两种写法: 先看看如何在方法上加锁:
这里加锁的目的是为了防止并发的情况下,创建了相同的目录,第二次会创建失败,影响业务功能。 但这种直接在方法上加锁,锁的粒度有点粗。因为doSave方法中的上传文件和发消息方法,是不需要加锁的。只有创建目录方法,才需要加锁。 我们都知道文件上传操作是非常耗时的,如果将整个方法加锁,那么需要等到整个方法执行完之后才能释放锁。显然,这会导致该方法的性能很差,变得得不偿失。 这时,我们可以改成在代码块上加锁了,具体代码如下:
这样改造之后,锁的粒度一下子变小了,只有并发创建目录功能才加了锁。而创建目录是一个非常快的操作,即使加锁对接口的性能影响也不大。 最重要的是,其他的上传文件和发送消息功能,任然可以并发执行。 当然,这种做在单机版的服务中,是没有问题的。但现在部署的生产环境,为了保证服务的稳定性,一般情况下,同一个服务会被部署在多个节点中。如果哪天挂了一个节点,其他的节点服务任然可用。 多节点部署避免了因为某个节点挂了,导致服务不可用的情况。同时也能分摊整个系统的流量,避免系统压力过大。 同时它也带来了新的问题:synchronized只能保证一个节点加锁是有效的,但如果有多个节点如何加锁呢? 答:这就需要使用: 由于zookeeper分布式锁的性能不太好,真实业务场景用的不多,这里先不讲。 下面聊一下redis分布式锁。 7.2 redis分布式锁在分布式系统中,由于redis分布式锁相对于更简单和高效,成为了分布式锁的首先,被我们用到了很多实际业务场景当中。 使用redis分布式锁的伪代码如下:
跟之前使用 其实只有创建目录的时候,才需要加分布式锁,其余代码根本不用加锁。 于是,我们需要优化一下代码:
上面代码将加锁的范围缩小了,只有创建目录时才加了锁。这样看似简单的优化之后,接口性能能提升很多。说不定,会有意外的惊喜喔。哈哈哈。 redis分布式锁虽说好用,但它在使用时,有很多注意的细节,隐藏了很多坑,如果稍不注意很容易踩中。详细内容可以看看我的另一篇文章《聊聊redis分布式锁的8大坑》 7.3 数据库分布式锁mysql数据库中主要有三种锁:
并发度越高,意味着接口性能越好。 所以数据库锁的优化方向是: 优先使用 赶紧看看,你用对了没? 8.分页处理有时候我会调用某个接口批量查询数据,比如:通过用户id批量查询出用户信息,然后给这些用户送积分。 但如果你一次性查询的用户数量太多了,比如一次查询2000个用户的数据。参数中传入了2000个用户的id,远程调用接口,会发现该用户查询接口经常超时。 调用代码如下:
众所周知,调用接口从数据库获取数据,是需要经过网络传输的。如果数据量太大,无论是获取数据的速度,还是网络传输受限于带宽,都会导致耗时时间比较长。 那么,这种情况要如何优化呢? 答: 将一次获取所有的数据的请求,改成分多次获取,每次只获取一部分用户的数据,最后进行合并和汇总。 其实,处理这个问题,要分为两种场景: 8.1 同步调用如果在 但对每一次远程接口调用的耗时有要求,不能大于500ms,不然会有邮件预警。 这时,我们可以同步分页调用批量查询用户信息接口。 具体示例代码如下:
代码中我用的 8.2 异步调用如果是在 除了需要考虑远程调用接口的耗时之外,还需要考虑该接口本身的总耗时,也不能超时500ms。 这时候用上面的同步分页请求远程接口,肯定是行不通的。 那么,只能使用 代码如下:
使用CompletableFuture类,多个线程异步调用远程接口,最后汇总结果统一返回。 9.加缓存解决接口性能问题, 但不能为了缓存而缓存,还是要看具体的业务场景。毕竟加了缓存,会导致接口的复杂度增加,它会带来数据不一致问题。 在有些并发量比较低的场景中,比如用户下单,可以不用加缓存。 还有些场景,比如在商城首页显示商品分类的地方,假设这里的分类是调用接口获取到的数据,但页面暂时没有做静态化。 如果查询分类树的接口没有使用缓存,而直接从数据库查询数据,性能会非常差。 那么如何使用缓存呢? 9.1 redis缓存通常情况下,我们使用最多的缓存可能是: 但对于java应用来说,绝大多数都是使用的redis,所以接下来我们以redis为例。 由于在关系型数据库,比如:mysql中,菜单是有上下级关系的。某个四级分类是某个三级分类的子分类,这个三级分类,又是某个二级分类的子分类,而这个二级分类,又是某个一级分类的子分类。 这种存储结构决定了,想一次性查出这个分类树,并非是一件非常容易的事情。这就需要使用程序递归查询了,如果分类多的话,这个递归是比较耗时的。 所以,如果每次都直接从数据库中查询分类树的数据,是一个非常耗时的操作。 这时我们可以使用缓存,大部分情况,接口都直接从缓存中获取数据。操作redis可以使用成熟的框架,比如:jedis和redisson等。 用jedis伪代码如下:
先从redis中根据某个key查询是否有菜单数据,如果有则转换成对象,直接返回。如果redis中没有查到菜单数据,则再从数据库中查询菜单数据,有则返回。 此外,我们还需要有个job每隔一段时间,从数据库中查询菜单数据,更新到redis当中,这样以后每次都能直接从redis中获取菜单的数据,而无需访问数据库了。 但这样做性能提升不是最佳的,还有其他的方案,我们一起看看下面的内容。 9.2 二级缓存上面的方案是基于redis缓存的,虽说redis访问速度很快。但毕竟是一个远程调用,而且菜单树的数据很多,在网络传输的过程中,是有些耗时的。 有没有办法,不经过请求远程,就能直接获取到数据呢? 答:使用 除了自己手写的内存缓存之后,目前使用比较多的内存缓存框架有:guava、Ehcache、caffine等。 我们在这里以 第一步,引入caffeine的相关jar包
第二步,配置CacheManager,开启EnableCaching
第三步,使用Cacheable注解获取数据
调用categoryService.getCategory()方法时,先从caffine缓存中获取数据,如果能够获取到数据,则直接返回该数据,不进入方法体。 如果不能获取到数据,则再从redis中查一次数据。如果查询到了,则返回数据,并且放入caffine中。 如果还是没有查到数据,则直接从数据库中获取到数据,然后放到caffine缓存中。 具体流程图如下: 由此可见,二级缓存给我们带来性能提升的同时,也带来了数据不一致的问题。使用二级缓存一定要结合实际的业务场景,并非所有的业务场景都适用。 但上面我列举的分类场景,是适合使用二级缓存的。因为它属于用户不敏感数据,即使出现了稍微有点数据不一致也没有关系,用户有可能都没有察觉出来。 10. 分库分表有时候,接口性能受限的不是别的,而是数据库。 当系统发展到一定的阶段,用户并发量大,会有大量的数据库请求,需要占用大量的数据库连接,同时会带来磁盘IO的性能瓶颈问题。 此外,随着用户数量越来越多,产生的数据也越来越多,一张表有可能存不下。由于数据量太大,sql语句查询数据时,即使走了索引也会非常耗时。 这时该怎么办呢? 答:需要做 如下图所示: 如果有用户请求过来的时候,先根据用户id路由到其中一个用户库,然后再定位到某张表。 路由的算法挺多的:
分库分表主要有两个方向: 说实话垂直方向(即业务方向)更简单。 在水平方向(即数据方向)上,分库和分表的作用,其实是有区别的,不能混为一谈。
如果在有些业务场景中,用户并发量很大,但是需要保存的数据量很少,这时可以只分库,不分表。 如果在有些业务场景中,用户并发量不大,但是需要保存的数量很多,这时可以只分表,不分库。 如果在有些业务场景中,用户并发量大,并且需要保存的数量也很多时,可以分库分表。 关于分库分表更详细的内容,可以看看我另一篇文章,里面讲的更深入《阿里二面:为什么分库分表?》 11. 辅助功能优化接口性能问题,除了上面提到的这些常用方法之外,还需要配合使用一些辅助功能,因为它们真的可以帮我们提升查找问题的效率。 11.1 开启慢查询日志通常情况下,为了定位sql的性能瓶颈,我们需要开启mysql的慢查询日志。把超过指定时间的sql语句,单独记录下来,方面以后分析和定位问题。 开启慢查询日志需要重点关注三个参数:
通过mysql的
设置完之后,如果某条sql的执行时间超过了2秒,会被自动记录到slow.log文件中。 当然也可以直接修改配置文件
但这种方式需要重启mysql服务。 很多公司每天早上都会发一封慢查询日志的邮件,开发人员根据这些信息优化sql。 11.2 加监控为了出现sql问题时,能够让我们及时发现,我们需要对系统做 目前业界使用比较多的开源监控系统是: 它提供了 架构图如下: 我们可以用它监控如下信息:
等等。。。 它的界面大概长这样子: 如果发现数据量连接池占用太多,对接口的性能肯定会有影响。 这时可能是代码中开启了连接忘了关,或者并发量太大了导致的,需要做进一步排查和系统优化。 截图中只是它一小部分功能,如果你想了解更多功能,可以访问Prometheus的官网:https://prometheus.io/ 11.3 链路跟踪有时候某个接口涉及的逻辑很多,比如:查数据库、查redis、远程调用接口,发mq消息,执行业务代码等等。 该接口一次请求的链路很长,如果逐一排查,需要花费大量的时间,这时候,我们已经没法用传统的办法定位问题了。 有没有办法解决这问题呢? 用分布式链路跟踪系统: 架构图如下: 之前没有这个功能的时候,为了定位线上接口性能问题,我们还需要在代码中加日志,手动打印出链路中各个环节的耗时情况,然后再逐一排查。 最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。 如果你用过skywalking排查接口性能问题,不自觉的会爱上它的。如果你想了解更多功能,可以访问skywalking的官网:https://skywalking.apache.org/ |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/24 3:08:59- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |