性能优化实践:一行代码性能提升几十倍?
性能优化实践:一行代码性能提升几十倍?
本文分享了一个核心接口的性能优化实践,通过采用科学法、USE方法和向下挖掘分析法,将响应时间从几百毫秒降低至几十毫秒,实现了数量级上的质变。文章详细描述了优化的背景、方法、分析过程和最终的解决方案,具有较高的技术价值和参考意义。
问题背景
手头有一个很重要的性能优化工作,需要静下心来不被打扰地进行分析。对于这个性能优化,提出了以下硬性要求:
- 效果要明显
- 改动要小,最好能用上之前同事做优化时添加代码的一些成果
性能优化方法
在《性能之巅》这本书中,印象最深的性能优化方法包括三种推荐的方法和三种要避免的方法,即“三正三反”。
三正
- 科学法:采用问题->假设->预测->实验->分析的框架
- USE方法:检查每个资源的使用率、饱和程度和错误情况
- 向下挖掘分析法:从高级别检查问题,然后缩小关注范围
三反
- 街灯讹:随意选择工具进行性能分析
- 随机变动讹:随机猜测问题位置并进行改动
- 责怪他人讹:将问题归咎于其他团队
分析过程
问题描述
有一个核心接口,一次调用涉及多次数据库查询,返回数据量在几百条的量级。网络正常情况下,第一次请求响应时间在1s3s之间。经过Redis缓存优化后,连续请求响应时间在630ms680ms之间。
假设
通过DBA的配合,将慢查询日志记录时间从1s以上调整为30ms以上。从慢查询日志可以看出,主要的性能瓶颈在数据库。第一次请求走数据库时耗时在500ms左右,后续走Redis缓存时调用耗时在200ms左右。
预测
为了降低网络开销,可以将使用Redis集中式缓存改成使用本地内存缓存。通过观察日志计算,抛去从数据库和缓存取数据的开销,其他时间开销在20多毫秒。据此预测,如果用内存缓存响应延迟可维持在35ms以下。
实验
通过修改Spring的Cacheable注解中的cacheManager实现,将Redis缓存改为本地内存缓存。运行结果显示响应时间从几百毫秒降低至几十毫秒,效果显著。
分析
- 本地缓存效果很好,但需要考虑数据一致性和持久化存储的需求
- 本次场景允许一定的数据不一致,不需要持久化存储
- 测试发现缓存数据量不会太大,对内存影响不大
- 还可以进一步优化异步获取数据等环节
总结
这次性能优化整体采用科学法的框架,结合USE方法排查隐患和向下挖掘分析法进行问题分析。过程中使用了业界常用的分析工具比如慢日志、JProfiler来做数据支撑。很多工作可以本地编写完了直接扔到服务器上测试。但是使用本地调试和扔到服务器上测试,养成的习惯和解决问题的方式,思考问题的缜密程度都截然不同。这些最终都决定了一个人的编程能力。
本篇文章中缓存使用的是ConcurrentMap。这个从性能上要比咱们耳熟能详的本地缓存大咖:Caffeine、Guava Cache要好。但只适用于测试,不建议生产环境使用。因为生产环境要考虑长期运行时的内存管理等问题。ConcurrentMap将存储所有存入的数据(本次测试场景入参是笛卡尔积之后也只有不到1千种情况,所以可以用),如果不引进额外的管理机制会导致OOM等问题。
以下是生产环境常用的本地缓存类库性能对比:
性能最好的Caffeine,业界是这样评价的:Caffeine是基于Java 1.8的高性能本地缓存库,由Guava改进而来,而且在Spring5开始的默认缓存实现就将Caffeine代替原来的Google Guava,官方说明指出,其缓存命中率已经接近最优值。实际上Caffeine这样的本地缓存和ConcurrentMap很像,即支持并发,并且支持O(1)时间复杂度的数据存取。二者的主要区别在于:
- ConcurrentMap将存储所有存入的数据,直到你显式将其移除
- Caffeine将通过给定的配置,自动移除“不常用”的数据,以保持内存的合理占用
因此,一种更好的理解方式是:Cache是一种带有存储和移除策略的Map。即:业界生产使用的最好的本地缓存组件的性能标杆是ConcurrentMap。