spring boot中集成了spring cache,并有多种缓存方式的实现,如:Redis、Caffeine、JCache、EhCache等等。但如果只用一种缓存,要么会有较大的网络消耗(如Redis),要么就是内存占用太大(如Caffeine这种应用内存缓存)。在很多场景下,可以结合起来实现一、二级缓存的方式,能够很大程度提高应用的处理效率。
内容说明:
缓存、两级缓存
spring cache:主要包含spring cache定义的接口方法说明和注解中的属性说明
spring boot + spring cache:RedisCache实现中的缺陷
caffeine简介
spring boot + spring cache 实现两级缓存(redis + caffeine)
缓存、两级缓存
简单的理解,缓存就是将数据从读取较慢的介质上读取出来放到读取较快的介质上,如磁盘-->内存。平时我们会将数据存储到磁盘上,如:数据库。如果每次都从数据库里去读取,会因为磁盘本身的IO影响读取速度,所以就有了像redis这种的内存缓存。可以将数据读取出来放到内存里,这样当需要获取数据时,就能够直接从内存中拿到数据返回,能够很大程度的提高速度。但是一般redis是单独部署成集群,所以会有网络IO上的消耗,虽然与redis集群的链接已经有连接池这种工具,但是数据传输上也还是会有一定消耗。所以就有了应用内缓存,如:caffeine。当应用内缓存有符合条件的数据时,就可以直接使用,而不用通过网络到redis中去获取,这样就形成了两级缓存。应用内缓存叫做一级缓存,远程缓存(如redis)叫做二级缓存
spring cache
当使用缓存的时候,一般是如下的流程:
从流程图中可以看出,为了使用缓存,在原有业务处理的基础上,增加了很多对于缓存的操作,如果将这些耦合到业务代码当中,开发起来就有很多重复性的工作,并且不太利于根据代码去理解业务。
spring cache是spring-context包中提供的基于注解方式使用的缓存组件,定义了一些标准接口,通过实现这些接口,就可以通过在方法上增加注解来实现缓存。这样就能够避免缓存代码与业务处理耦合在一起的问题。spring cache的实现是使用spring aop中对方法切面(MethodInterceptor)封装的扩展,当然spring aop也是基于Aspect来实现的。
spring cache核心的接口就两个:Cache和CacheManager
Cache接口
提供缓存的具体操作,比如缓存的放入、读取、清理,spring框架中默认提供的实现有:
除了RedisCache是在spring-data-redis包中,其他的基本都是在spring-context-support包中
CacheManager接口
主要提供Cache实现bean的创建,每个应用里可以通过cacheName来对Cache进行隔离,每个cacheName对应一个Cache实现。spring框架中默认提供的实现与Cache的实现都是成对出现,包结构也在上图中
常用注解说明
@Cacheable:主要应用到查询数据的方法上
@CacheEvict:清除缓存,主要应用到删除数据的方法上。相比Cacheable多了两个属性
@CachePut:放入缓存,主要用到对数据有更新的方法上。属性说明参考@Cacheable
@Caching:用于在一个方法上配置多种注解
@EnableCaching:启用spring cache缓存,作为总的开关,在spring boot的启动类或配置类上需要加上此注解才会生效
spring boot + spring cache
spring boot中已经整合了spring cache,并且提供了多种缓存的配置,在使用时只需要配置使用哪个缓存(enum CacheType)即可
spring boot中多增加了一个可以扩展的东西,就是CacheManagerCustomizer接口,可以自定义实现这个接口,然后对CacheManager做一些设置,比如:
加载这个bean:
常用的缓存就是Redis了,Redis对于spring cache接口的实现是在spring-data-redis包中
这里提下我认为的RedisCache实现中的缺陷:
1.在缓存失效的瞬间,如果有线程获取缓存数据,可能出现返回null的情况,原因是RedisCache实现中是如下步骤:
判断缓存key是否存在
如果key存在,再获取缓存数据,并返回
因此当判断key存在后缓存失效了,再去获取缓存是没有数据的,就返回null了。
2.RedisCacheManager中是否允许存储空值的属性(cacheNullValues)默认为false,即不允许存储空值,这样会存在缓存穿透的风险。缺陷是这个属性是final类型的,只能在创建对象是通过构造方法传入,所以要避免缓存穿透就只能自己在应用内声明RedisCacheManager这个bean了
3.RedisCacheManager中的属性无法通过配置文件直接配置,只能在应用内实现CacheManagerCustomizer接口来进行设置,个人认为不太方便
Caffeine
Caffeine是一个基于Google开源的Guava设计理念的一个高性能内存缓存,使用java8开发,spring boot引入Caffeine后已经逐步废弃Guava的整合了。Caffeine源码及介绍地址:caffeine
caffeine提供了多种缓存填充策略、值回收策略,同时也包含了缓存命中次数等统计数据,对缓存的优化能够提供很大帮助
caffeine的介绍可以参考:http://www.cnblogs.com/oopsguy/p/7731659.html
这里简单说下caffeine基于时间的回收策略有以下几种:
expireAfterAccess:访问后到期,从上次读或写发生后的过期时间
expireAfterWrite:写入后到期,从上次写入发生之后的过期时间
自定义策略:到期时间由实现Expiry接口后单独计算
spring boot + spring cache 实现两级缓存(redis + caffeine)
本人开头提到了,就算是使用了redis缓存,也会存在一定程度的网络传输上的消耗,在实际应用当中,会存在一些变更频率非常低的数据,就可以直接缓存在应用内部,对于一些实时性要求不太高的数据,也可以在应用内部缓存一定时间,减少对redis的访问,提高响应速度
由于spring-data-redis框架中redis对spring cache的实现有一些不足,在使用起来可能会出现一些问题,所以就不基于原来的实现去扩展了,直接参考实现方式,去实现Cache和CacheManager接口
还需要注意一点,一般应用都部署了多个节点,一级缓存是在应用内的缓存,所以当对数据更新和清除时,需要通知所有节点进行清理缓存的操作。可以有多种方式来实现这种效果,比如:zookeeper、MQ等,但是既然用了redis缓存,redis本身是有支持订阅/发布功能的,所以就不依赖其他组件了,直接使用redis的通道来通知其他节点进行清理缓存的操作
以下就是对spring boot + spring cache实现两级缓存(redis + caffeine)的starter封装步骤和源码
定义properties配置属性类
spring cache中有实现Cache接口的一个抽象类AbstractValueAdaptingCache,包含了空值的包装和缓存值的包装,所以就不用实现Cache接口了,直接实现AbstractValueAdaptingCache抽象类
实现CacheManager接口
redis消息发布/订阅,传输的消息类
监听redis消息需要实现MessageListener接口
增加spring boot配置类
在resources/META-INF/spring.factories文件中增加spring boot配置扫描
接下来就可以使用maven引入使用了
在启动类上增加@EnableCaching注解,在需要缓存的方法上增加@Cacheable注解
properties文件中redis的配置跟使用redis是一样的,可以增加两级缓存的配置
扩展
个人认为redisson的封装更方便一些
对于spring cache缓存的实现没有那么多的缺陷
使用redis的HASH结构,可以针对不同的hashKey设置过期时间,清理的时候会更方便
如果基于redisson来实现多级缓存,可以继承RedissonCache,在对应方法增加一级缓存的操作即可
如果有使用分布式锁的情况就更方便了,可以直接使用Redisson中封装的分布式锁
redisson中的发布订阅封装得更好用
后续可以增加对于缓存命中率的统计endpoint,这样就可以更好的监控各个缓存的命中情况,以便对缓存配置进行优化
源码
https://gitee.com/itopener/springboot
starter目录:springboot / itopener-parent / spring-boot-starters-parent / cache-redis-caffeine-spring-boot-starter-parent
示例代码目录: springboot / itopener-parent / demo-parent / demo-cache-redis-caffeine
参考资料
http://jinnianshilongnian.iteye.com/blog/2001040
https://coderknock.com/blog/2017/03/01/SpringBoot%EF%BC%8C%E7%94%A8200%E8%A1%8C%E4%BB%A3%E7%A0%81%E5%AE%8C%E6%88%90%E4%B8%80%E4%B8%AA%E4%B8%80%E4%BA%8C%E7%BA%A7%E5%88%86%E5%B8%83%E5%BC%8F%E7%BC%93%E5%AD%98.html
http://www.cnblogs.com/oopsguy/p/7731659.html
https://github.com/ben-manes/caffeine
领取专属 10元无门槛券
私享最新 技术干货