天天说要做性能优化,到底在优化什么?
面试过程中经常被问到:
- 你做过性能优化吗?
- 优化了哪些方面?
- 怎么做优化的?
- 优化的效果如何?
连环炮问下来,对于有做过优化的老司机来说,肯定能抗住。对于没有真正做过优化的小白来说,肯定扛不住这一系列的追问,最后只能以面试失败而告终。
那么性能优化到底在优化什么呢?我们来盘点下一些常用的优化手段。
SQL 优化
当你开发的接口响应时间超过了 200ms 的时候就得优化了,当然 200ms 不是绝对值,具体还是看应用场景。以 App 举例,进一个页面调用 5 个接口(题外话:也可以做聚合),那么总共就是 1s 的时间,对用户来说体验还算可以,当然是越快响应越好。
接口耗时 200ms,其中占大头的还是对数据库的操作,一个接口中会有 N 次数据库操作。所以优化 SQL 的速度优先级是最高的,大量的慢 SQL 会拖垮整个系统。
关于 SQL 的优化不是本文的重点,大部分慢 SQL 还是跟各位平时开发时的习惯有关系。大部分在写 SQL 的时候不太会去考虑性能,只要写出来就可以了,join 随手就来,也不梳理查询字段,不加索引,刚开始上线没问题,等到并发量,数据量起来的时候就凉凉了。
关于数据库的使用规范大家可以参考下这篇文章:https://mp.weixin.qq.com/s/mFsK7YSKcG6T7jpPnK92tg
当数据量大了后肯定要做读写分离和分库分表的,这也是优化的必经之路。相关的文章也可以参考我之前写的一些:http://mp.weixin.qq.com/mp/homepage?__biz=MzIwMDY0Nzk2Mw==&hid=4&sn=1b96093ec951a5f997bdd3225e5f2fdf&scene=18#wechat_redirect
减少重复调用
性能不好的另一个致命问题就是重复调用,相同的逻辑在不同的方法中重复对数据库查询,重复调用 RPC 服务等。
比如下面的代码:
skuDao.querySkus(productId).stream().map(sku -> {
skuDao.getById(sku.getId());
})
明明数据已经查询出来了,又根据 ID 重新去查询了一次,数量越多,浪费的时间越多。这里只是举例,我相信在真实的项目中大量存在重复查询的情况,之前我还写过一篇文章,讲解如何解决这种重复查询问题,感兴趣的可以查看这篇文章:https://mp.weixin.qq.com/s/1k4OtNYIoOasrXAF1ZhcGg
按需查询
很多业务逻辑不复杂的功能,却响应很慢。往往都是写代码的时候没有思考,随便就调用一些已经存在的方法,导致整体响应变慢,总结起来就是:性能问题大部分都是代码写出来的。
说个场景,大家肯定都见到过。参数是一个商品 ID, 功能是上架商品,需要进行状态的判断,符合条件才能上架。这个场景下只需要获取商品的状态进行判断即可,有的时候你看到的代码往往都是下面的方式:
GoodsDetail goods = goodsService.detail(id);
if (goods.getStatus() == GoodsStatusEnum.XXXX) {
}
detail 中有大量的逻辑,除了基本的商品信息,还有很多其他的内容,这就是慢的原因。
并行调用
针对一个接口,如果设计到多个内部 RPC 服务或者多个外部接口,在接口之间没有关联关系的情况下,我们可以采用并行调用的方式来提高性能。
CompletableFuture 就非常适合并行调用的场景,关于 CompletableFuture 的使用本文不做详细说明,做 Java 的都要会用。
除了 CompletableFuture 之外,对于集合类的处理,可以用 parallelStream 来实现并行调用。
在微服务中有一层专门用于聚合 API, 聚合层就非常适合并行调用,一个功能或者一个页面展示会涉及到多个接口,通过聚合层在后端进行接口的聚合和数据的裁剪,一起响应给前端。
上缓存
缓存也是优化中最常用的,效果提升最明显的,成本也不大。对于缓存,也不要滥用,不是所有场景都可以靠堆缓存来提高性能的。
首先对于实时性要求不高的业务场景可以优先使用缓存,也不用太考虑更新的问题,自然过期就行。
实时性要求高的业务场景,用缓存一定要有完整的缓存更新机制,否则很容易造成业务数据和缓存数据不一致的情况。
建议的做法是订阅 binlog 来统一更新缓存,不要在代码中去更新或者失效缓存,简单的场景还好,入口就那几个,问题不大。有些数据在多个场景下使用,需要更新的入口太多了,
异步处理
有些逻辑,不需要实时反馈给用户那就可以采用异步的方式在后台进行处理。
异步处理的方式最常见的就是将任务加到线程池中进行处理,线程池需要考虑容量以及对一些指标的监控,相关的文章可以查看我的这篇:https://mp.weixin.qq.com/s/JM9idgFPZGkRAdCpw0NaKw
除了一些指标的监控,线程池的使用另一个需要关注的问题就是任务的持久化。如果你的数据本来就是存储好了的,然后读取出来通过线程池去执行是没问题的。如果是没有持久化直接丢入线程池中进行执行,就有可能出现丢失的情况,比如服务重启之类的场景。
关于持久化,无论是线程池还是 EventBus 这种,都会遇到,所以针对异步的场景我建议大家使用消息队列比较好。
消息队列可以存储任务信息,保证不会丢失。单独消费队列的消息进行逻辑处理,如果想提高消费速度,也可以在队列的消费方使用线程池进行多线程消费,多线程消费也要避免消息丢失的情况,可以查看我的这篇文章:https://mp.weixin.qq.com/s/Bbh1GDpmkLhZhw5f0POJ2A
JVM 参数调整
JVM 参数的调整,一般情况下我们都不用怎么去调整。偶尔有些代码写的不好,导致内存溢出了,这个时候会去做一些调整和优化代码。
参数调整主要是去降低 GC 的导致的停顿问题,如果你的程序一直在 GC, 一直在停顿,你的接口自然就慢了。
只要没有频繁的 Full GC,在优化这块 JVM 的参数调整可以最后再做,优先以 SQL 优化这些为主。
加机器
加机器是最后的终极大招了,并发量上去的时候,你在怎么优化单机器和单数据库抗并发能力也是有限的,这个时候只能水平扩展了。
如果是创业初期,并且在快速发展,加机器是最直接的优化方式了,虽然说成本上去了,但是开发资源也是成本,节约下来可以实现更多的业务需求。等到中期稳定了再考虑架构,性能方面整体的优化和重构。
就像玩游戏一样,有装备的玩家才能所向睥睨啊,对于后端应用来说也是一样,高配的机器,高配的数据库配置,高配的缓存等。
关于作者:尹吉欢,简单的技术爱好者,《Spring Cloud 微服务-全栈技术与案例解析》, 《Spring Cloud 微服务 入门 实战与进阶》作者, 公众号猿天地发起人。
我整理了一份很全的学习资料,感兴趣的可以微信搜索「猿天地」,回复关键字 「学习资料」获取我整理好了的 Spring Cloud,Spring Cloud Alibaba,Sharding-JDBC 分库分表,任务调度框架 XXL-JOB,MongoDB,爬虫等相关资料。