一文深入了解史上最强的Java堆内缓存框架Caffeine

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

一文深入了解史上最强的Java堆内缓存框架Caffeine

码大叔   2020-03-15 我要评论
> 它提供了一个近乎最佳的命中率。从性能上秒杀其他一堆进程内缓存框架,Spring5更是为了它放弃了使用多年的GuavaCache 缓存,在我们的日常开发中用的非常多,是我们应对各种性能问题支持高并发的一大利器。我们熟知的缓存有堆缓存(Ehcache3.x、Guava Cache等)、堆外缓存(Ehcache3.x、MapDB等)、分布式缓存(Redis、 memcached等)等等。今天要上场的主角是Caffeine,它其实是Google基于Java8对GuavaCache的重写升级版本,支持丰富的缓存过期策略,尤其是TinyLfu 淘汰算法,提供了一个**近乎最佳的命中率**。从性能上(读、写、读/写)也足以**秒杀**其他一堆进程内缓存框架。Spring5更是直接放弃了使用了多年的Guava,而采用了Caffeine。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200315122650524.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0phNW9u,size_16,color_FFFFFF,t_70)(以上数据来自官方读写性能测试结果,更多测试结果详见 https://github.com/ben-manes/caffeine/wiki/Benchmarks) 当然在实际使用中基本会涉及中多个缓存的组合使用,比如二级缓存(Caffeine+Redis)、多级缓存等等,这个以后再讲。接下来我们分【基础实战】、【高阶用法】、【理论概述】三个部分来聊一聊史上最强的Java堆内缓存框架。 (在“码大叔”公众号回复数字136即可获取演示源码及牛逼的TinyLfu论文。论文版权归原作者所有,向大神学习致敬) # ====基础实战==== 接下来我们通过一些例子来演示Caffeine的基础用法,首先我们通springboot新建一个mds-caffeine-demo的Gradle工程。 ### 一、基础配置 #### 1、添加依赖 需要使用到 spring-boot-starter-cache和caffeine两个包 ```java implementation 'org.springframework.boot:spring-boot-starter-cache' implementation 'com.github.ben-manes.caffeine:caffeine' ``` #### 2、在applicationyml文件中添加配置 ```yaml spring: cache: type: caffeine ``` #### 3、添加注解 在启动类上添加@EnableCaching ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200315123149466.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0phNW9u,size_16,color_FFFFFF,t_70) 就是这么地 so easy,Caffeine就已经集成到我们的项目中来了。 ## 二、实战演示 假设我们数据库中有一张User表,里面有【码大叔和小九九】2条数据 |id| name |birdhtday| |--|--|--| |1 | 码大叔 |2012-05-12| |2 | 小九九 |1999-09-19| ### 场景1:添加及使用缓存 只需要使用@Cacheable注解即可自动将数据添加到缓存中,后续直接从缓存中读取数据。 **value**:表示缓存的名称,这个参数value还是比较误导人的,不是缓存的值,所以官方还提供了一种写法:cacheNames。 **key**:表示缓存的key,可以为空。如果指定需要按照SpEL表达式编写 #### 方法1、将用户对象以ID作为key存放到缓存中。![在这里插入图片描述](https://img-blog.csdnimg.cn/20200315123520904.png) 我们访问页面: ![在这里插入图片描述](https://img-blog.csdnimg.cn/2020031512353339.png) 第一次:打印了数据库操作的日志 ![在这里插入图片描述](https://img-blog.csdnimg.cn/2020031512355379.png) 第二次:没有打印,表示缓存添加成功。 ### 方法2、将满足条件的数据存放到缓存中 @Cacheable有一个参数叫做condition,该条件为true时则放到缓存到。该参数同样需使用SpEL表达式。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200315123618888.png) 接下来我们分别进行用户1、用户2、用户1、用户2 四次查询。我们看到只打印了3条数据,第二次访问用户1从缓存中读取数据,用户2每次都是从数据库中读取数据,没进入缓存。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200315123632743.png) **【敲黑板】** - 还有一个条件参数unless,与condition的用法恰好相反。 - 使用了条件式缓存后,哪怕哪怕缓存里已经有数据了,也依然会跳过缓存。比如我们在其他方法中将“小九九”添加到了缓存中,但通过该方法获取小九九的数据时,依然是从数据库中取值。 - @Cacheable注解不仅仅可以标记在一个方法上,还可以标记在一个类上,表示该类所有的方法都是支持缓存的。 - 我们除了使用参数作为key之外,Spring还为我们提供了一个root对象可以用来生成key,比如 #root.methodName(当前方法名), #root.target(当前被调用的对象), #root.args[0]( #root.args[0])等等。 ### 场景2:更新缓存 使用@CachePut,添加了该注解后每次都会触发真实方法的调用 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200315123810804.png) 我们觉得码大叔的年龄可能造假了,怎么可能是2012年,把它更新为真实的年龄。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200315123831146.png) 我们看到数据库层面打印了日志。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200315123855343.png) 此时我们再访问获取用户信息方法,已经获取到了最新的数据,但服务端却没有任何日志。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200315123935275.png) 这表明该注解已帮我们把最新的信息更新到了缓存中。 **【敲黑板】** - 在方法上使用了@CachePut注解如果方法返回了void或者null,也会同步更新缓存,缓存的对象为空,所以使用时务必要注意。缓存默认是支持存储nul的,这也符合我们使用缓存的诉求。如果在某些特殊的场景下不希望缓存null对象,可以使用condition条件:condition = "#result != null" 即可。 ### 场景3:删除缓存 使用@CacheEvict注解,可以手动将对象从缓存中删除。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200315124025549.png) 比如上面的方法,表示将指定id的用户从缓存中删除。如果期望将USER的所有缓存删除,则可以使用参数 allEntries = true(默认为false) 即可。 **【敲黑板】** - 如果方法里有代码逻辑,那么是先删除缓存还是先执行方法呢?答案是先执行方法,后清除缓存。如果期望先清除缓存后执行方法,则添加参数 beforeInvocation = true即可。 # ==高阶用法== ## 1:线程锁定 前面我们提到了@Cacheable可以添加缓存,当缓存过期之后如果多个线程同时请求过来,而该方法执行较慢时可能会导致大量请求堆积,甚至导致缓存瞬间被击穿,所有请求同时去到数据库,数据库瞬间负荷增高。所以该注解还提供了一个参数 sync:默认为false,如果为true时表示多个线程同时调用此时只有一个线程能够成功调用,其他线程直接取这次调用的返回值。不过它在代码注释上也写了,这仅仅是个hint,具体还是要看缓存提供者。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200315124207128.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0phNW9u,size_16,color_FFFFFF,t_70) 不管sync设置是true还是false,Caffeine默认使用的都是单线程 :只允许一个线程去加载数据,其余线程阻塞。这样其实也会导致效率低下,用户等待。因此建议配合refreshAfterWrite一起使用:只阻塞加载数据的线程,其余线程返回旧数据。 ## 2:缓存失效 初始化缓存时,我们还可以设置3个参数:expireAfterAccess、expireAfterWrite、refreshAfterWrite。千万不要被这三个单词的表面意思误导,网上很多写法也是错的。比如expireAfterAccess,不是表示访问完多长时间就过期,而是多长时间没有访问就失效。 - expireAfterAccess=[duration]:指在指定时间内没有被读或写就回收 - expireAfterWrite=[duration]: 指在指定时间内没有被创建或覆盖就回收 - refreshAfterWrite=[duration]:指在指定时间内没有被创建/覆盖,则指定时间过后再次访问时会去刷新该缓存,在新值没有到来之前,始终返回旧值 我们以expireAfterWrite为例,配置如下,然后不停地访问,我们看到每隔5秒后就自动更新一次缓存。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200315124306146.png)![在这里插入图片描述](https://img-blog.csdnimg.cn/20200315124318126.png) **【敲黑板】** - 如果是yml文件要注意写法,这几个都是spec的value值,caffeine会自行解析,不要像下面这种写法,是错误的。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200315124408678.png) - 以expireAfterWrite为例,假设设置的是5秒,并不是指5秒后自动更新,而是在5秒后的下一次访问时才更新 - 如果expireAfterWrite和expireAfterAccess同时存在,以expireAfterWrite为准。 ## 3:refreshAfterWrite 这个参数在前面也提到了在日常使用中用的比较多,尤其是对于互联网高并发的场景,所以额外再补充讲几点。 1、使用了refreshAfterWrite后,启动项目会报如下的错误, ```java 2020-03-08 13:51:51,144|o.s.boot.SpringApplication|reportFailure|Application run failed org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'cacheManager' defined in class path resource [org/springframework/boot/autoconfigure/cache/CaffeineCacheConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.cache.caffeine.CaffeineCacheManager]: Factory method 'cacheManager' threw exception; nested exception is java.lang.IllegalStateException: refreshAfterWrite requires a LoadingCache at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:656) at com.qiaojs.mds.MDSApplication.main(MDSApplication.java:16) Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.cache.caffeine.CaffeineCacheManager]: Factory method 'cacheManager' threw exception; nested exception is java.lang.IllegalStateException: refreshAfterWrite requires a LoadingCache ... 19 common frames omitted Caused by: java.lang.IllegalStateException: refreshAfterWrite requires a LoadingCache ... 20 common frames omitted ``` 这需要我们去实现一个CacheLoader,再重启就OK了。 ```java @Bean public CacheLoader

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们