最近我在搞一个小小的采用springboot的单体构架的demo,本来是想着把demo搞出来,结果不知不觉就变成“把demo做得更快”了( )。虽然没啥技术含量,而且还浪费了那么多时间,但是不记下来的话就一点用都没有了对吧。所以这就是我写这篇水文的原因。

要让系统变得更快,其实从理论上讲,无非就两点:

  1. 减少时间复杂度。
  2. 利用缓存特性。

减少时间复杂度,基本就是精简一些逻辑,或者采用不同的算法。

比如,要判断url是否需要被拦截,有两种办法,第一种是用正则表达式验证,第二种呢,则是把这个server的所有端点拿出来,把它们的url先验证一遍,分成需要验证的和不需要验证的,分别放在一个set里,然后每次去set里找它在不在。显然,第二种的时间复杂度要低太多太多。

利用缓存特性,简单来说就是,根据不同层存储器的速度不同,尽量利用离cpu近的存储器,少用离cpu远的存储器。由于Java对CPU的L1还有L2的利用我很难控制,所以问题基本集中在一点,就是多用内存,少走磁盘IO(网络IO,哪怕是本机的网络IO也尽量避免)。

当然,针对L1和L2的问题,也不是不能优化。但基本来说,也就是尽可能缩小数据的大小,让数据能一次装进小小的cache里——那么很明显了,我们在能用基本数据类型的地方(能用的含义是保证业务逻辑安全,保证使用的工具不会因此出bug!比如可选的参数就不能使用基本数据类型!),最好不要用非基本数据类型

从复杂点的角度来说,利用缓存特性不止上面这两种思路。我们知道,Java里大部分对象都是朝生夕灭,其中甚至不乏一些大对象。而对象的创建和回收是很浪费资源的事情。因此,我们在能重用对象的地方,最好重用对象。这样,就能减少对象的创建和回收频率。

当然,这句话的最佳实践是很难的事情。首先,能重用的对象,一般来说必须是不可变的(或者说不会变的),而且必须考虑线程安全问题。如果一个重用的对象在不断变化,那很可能会带来一些不可预知的问题——线程安全也一样,因为我们知道,往往要求线程安全也就是要求事实上的对象不可变(有一点需要指出的是,有的线程安全实现不是那么好,比如对象的方法里有大量的锁,此时如果缓存起来公用,锁竞争太激烈反而降低性能,所以是否缓存取决于测试的结果)。

其次,也是往往会忽视却不得不考虑的问题,就是这些对象如果长期持有不释放,那么有没有可能会在某种情况下占据大量内存导致jvm可用内存太少?目前而言,我遇到过两种情况:第一是对象的创建数量不可预知,有可能只创建有限对象,但也有可能创建很多对象,必须限制缓存对象的数量;第二是对象数量虽然可预知,但数量大,对象大,因此内存占用也大。这两种情况都必须避免。

在以上两条指导原则的引导下,我对springboot做了以下事情:

1.我的application采取全json返回的方式,所有数据均使用json返回。我使用的json解析器是jackson,因为它功能最全bug最少且性能并不比fastjson之类差。然后我将json解析器的使用进行了封装。首先,jackson提供的API既支持直接的json——对象序列化和反序列化,也支持使用流式API包装后序列化和反序列化,后者比前者快好几倍。其次,针对常用到的几种情况,我将ObjectReader和ObjectWriter缓存起来,每次使用缓存的Reader和Writer进行json序列化和反序列化。据测试,反序列化性能大概提高了4倍左右。序列化的性能没测,但估计也快了好几倍。

2.我的application使用了spring security安全框架。spring security需要在每次请求时对其进行鉴权,那么就需要获取某种请求对应的权限来比对。显然,这里加入缓存是非常必要的选择。

3.application中有用到AOP的地方,这些地方往往大量用到反射。事实上spring全家桶里已经无缝集成了cglib,在使用反射的地方,我尽量使用cglib辅助,并且对cglib的使用同样采用了缓存的办法。当然,缓存的对象必须是事实上不可变的,而字节码增强往往是有针对性的,可能随时会改变,因此不能滥用。同时,项目中往往会遇到一个bean的字段复制到另一个bean的场景,这种时候使用cglib的BeanCopier比BeanUtils.copyProperties快,大概快2.5-3倍左右。需要注意的是,BeanCopier的实例也要缓存,否则每次创建实例的开销比反射还要大,得不偿失。

4.考虑到application基本只用于单体架构,且目前暂未遇到缓存持久化的需求,在业务逻辑中需要用到缓存的地方,我暂时都使用了jvm内存缓存,而不是redis。虽然redis很快,而且其热点数据也存在于内存中,但不得不提的是,与redis的通信,大头其实是网络——相反,jvm内部的缓存,就完全不需要走网络,这带来的是redis不可能赶上的速度。有时候的缓存需要用到诸如自动过期,大小限制等功能,这时可以使用guava的cache来实现。

5.对于springboot原有的一些逻辑,可以尝试重写。比如之前优化jackson的时候,发现springboot默认的jacksonHttpMessageConverter里逻辑很慢很啰嗦,于是我将其替换成了我自己的实现,针对自己的项目进行了功能的裁剪,并且利用缓存将每次都会创建的ObjectReader,ObjectWriter以及其他的大对象都缓存起来。这个转换器还需要用到canWrite和canRead方法,同样地,把这两个方法的结果缓存上。

6.有的逻辑,可以使用初始化的方式来简化计算量。比如前面举的例子,需要判断访问某个url时是否应该验证用户身份,第一种办法是用正则表达式等方式,每次去计算去判断。但第二种不一样。我们知道系统中的url数量是有限的,那么,每次启动的时候,拿到所有的url,先判断一次,存到一个set里,那么判断是否应该验证的时候,就不再是用正则表达式了,而变成了判断set里是否存在这个值。显而易见,后者是O(1),前者至少是O(N)。

其实在这里可以总结一下,在保证业务逻辑正确的前提下,对于可预知的结果,我们完全可以使用初始化的方式降低时间复杂度;对于不可预知的结果,根据逻辑判断是否可以缓存,并且处理好不可变,线程安全以及内存占用。

好了,其实项目里还有很多很多优化,但如果都用文字来叙述,一来太冗长,二来也难以描述清楚细节。最后我还是放上这个项目的github地址,各位如果想仔细分析,就请进代码里看吧。


Frodez/BlogManagePlatformgithub.com


spring boot 性能如何优化 springboot项目内存性能优化_spring boot 性能如何优化