Spring Cache本身是一个缓存体系的抽象实现,并没有具体的缓存能力,要使用Spring Cache还需要具体的缓存实现来完成;也就是我们说的cache storage。Spring Boot 集成了多种cache的实现,如果你没有在配置类中声明CacheManager或者CacheResolvoer,那么SpringBoot会按顺序在下面的实现类中寻找:Generic、JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)、EhCache 2.x、Hazelcast、Infinispan、Couchbase、Redis、Caffeine、Simple。
一、如何在代码中配置缓存
1)首先需要引入依赖spring-boot-starter-cache,其中包含CacheAutoConfiguration类,提供基于convention的配置,并允许customization。
2)其次,在启动类中用@EnableCaching开启缓存,启动类指的是main函数所在的类,也是我们通常所说的配置类(@Configuration);@EnableCaching会触发一个post processor,扫描每一个spring bean,查看是否已经存在缓存;如果找到了,就会自动创建一个代理拦截方法调用。
3)最后,配置cache manager用于管理缓存;cache manager有多种实现,最近项目中使用的是SimpleCacheManager,缓存的具体实现也有很多种,最近项目使用的是caffein。
4)缓存配置属性:cache.specs.<cacheName>.expire-after-write表示缓存过期时间,可以指定时间单位,通常是秒;cache.specs.<cacheName>.maximum-size表示可以存储多少缓存项,单位是个。
二、如何在代码中使用缓存
2.1 @Cacheable("cache name"、key、condition)
被@Cacheable标注的方法首先检查缓存中是否存在要查询的数据,如果有则直接返回,没有则调用方法查询,并把请求参数和返回值作为键值对存储在缓存中。
key可以在注解中用spEL表达式指定,如当参数值为整形ID,则可以指定为#id或#p0,如参数是对象类型,可指定为#obj.id;没有指定则默认使用被标注方法的参数作为key,单个参数则该参数值为key,多个参数则哈希值为key,没有参数则默认0作为key。
缓存条件分为基于输入或基于输出的条件;基于输入的条件用condition限定,使用spEl表达式编写,值为true则缓存,否则不缓存;基于输出的条件可以用unless限定。
2.2 清空缓存
缓存空间有限,清空缓存可以清除不再需要的缓存项;或用于配合更新缓存。
1)@CacheEvict(name="cache name", key=xx, condition=xx):cachename至少指定一个;key用spEL表达式编写,如果为空则按照所有参数进行组合定义;condition用spEl表达式编写,结果为“true”清空缓存,“false”则不清空;beforeInvocation:是否在程序执行前清空缓存,为true时则在程序执行前清空缓存,默认为false;allEntries:是否清空所有缓存,用spEl表达式编写,当程序执行时,为true则全部清空缓存,false则不全部清空,默认为false。
2)调用cache manager提供的evict方法,传入key;可以结合spring task scheduler,定时清理缓存;
3)调用cache manager提供的clear方法,清空全部缓存项。
2.3 更新缓存
@CachePut:@Cacheable和@CachePut的区别在于,@Cacheable只在结果未被缓存时执行方法并把结果放入缓存,当结果已经被缓存则不再调用方法直接返回缓存中的结果。 @CachePut必然会执行方法并把结果放入缓存,用于不是每次必然执行,一旦执行就需要更新缓存的方法。
考虑到并发下的竞争,通常更推荐清空缓存而不是更新缓存的方式。
2.4 其他工具类注解
@Caching:group multiple caching annotations,java编译器不允许一个方法有多个同类型注解。例如,我想着执行方法时填充到缓存1,并清除缓存2,并更新缓存3。
@CacheConfig:在类级别设置缓存通用信息,如cache name,这样在方法级别就不需要重复设置。
三、缓存的底层实现与常见问题
3.1 ConcurrentHashMap
请看另一篇文章
3.2 动态代理
Spring cache是基于Spring Aop的动态代理机制来对方法的调用进行切面,这里关键点是对象的引用问题,如果对象的方法是内部调用(即this引用)而不是外部引用,则会导致 proxy 失效,那么我们的切面就失效,也就是说上面定义的各种注释包括 @Cacheable、@CachePut 和 @CacheEvict 都会失效。解决方案要么两个方法放在不同的类,要么通过applicationContext.getBean(ClassName)的方式获取到实例再调用方法。
最近项目上的遇到了一个问题就是因为有同事犯了上述的错误,让方法A和方法B处于一个类中,当方法A调用方法B时,方法B声明的缓存不生效,导致页面访问速度明显变慢;更严重的是,这个问题直到上了生产环境才暴露出来。其影响不仅仅是让我们的应用响应速度变慢,也会发送更多请求到第三方系统,导致其压力增大。这个问题如何解决并不复杂,但是我们花费了很多时间来retro问题发生的原因,以及我们可以怎样优化流程并避免此类问题发生。
验尸报告如下:
1)开发对底层知识需要加强掌握,不仅限于业务代码和框架使用,框架使用只能告诉我们正确的用法,人不是机器,不可能背过一切,框架使用规则的背后,是java/jvm等知识,理解了为什么才能理解的更透彻,把知识真正变成自己的。这也是很大大公司着重考察数据结构等的原因。改进措施是继续搞读书分享会,提高团队的学习技术氛围。
2)我们基本上每天都会做code review,为什么代码评审没有发现问题?首先,我们相信大家都认真在看,因此代码评审需要更认真,这个没什么价值;但是我们可以吸取的经验教训是:小步提交。包含bug的代码提交里包含46个文件的修改。小步提交使得review代码的时候我们的注意力更集中的探讨一个修改。
3)为什么测试没有发现问题?就像测试金字塔一样,我们有不同层级的测试;从层级角度来说,首先,没有自动化针对缓存的测试,这个是必须要加上的;其次,手动测试没有对缓存的验证,我的想法是如果某个用户故事涉及了缓存的修改,那么开发需要自己检查日志,并截图缓存有效作为附近放在用户故事里面;第三,我们有没有可能引入性能测试,在哪个环境引入,在哪个层级引入。
3.3 缓存数据不一致
spirng cache是内存型缓存,当有多台服务器时,可能存在数据不一致;比如获取商店信息有缓存,缓存了商店状态A;此时更新商店状态到B,请求发送到了Server A,则server A的缓存被清空;但Server B的缓存为状态A;此时用户请求查看商户,如果请求发送到Server A,则返回最新状态B,如果请求发送到Server B,则返回状态A。
网友评论