SpringBoot 如何快速使用 Caffeine 缓存?

时间:2022-09-25 13:11:40

SpringBoot 如何快速使用 Caffeine 缓存?

引言

前面我们有学习Caffeine 《本地缓存性能之王Caffeine》,并且也提到SpringBoot默认使用的本地缓存也是Caffeine啦,今天我们来看看Caffeine如何与SpringBoot集成的。

集成caffeine

caffeine与SpringBoot集成有两种方式:

  • 一种是我们直接引入 Caffeine 依赖,然后使用 Caffeine 方法实现缓存。相当于使用原生api
  • 引入 Caffeine 和 Spring Cache 依赖,使用 SpringCache 注解方法实现缓存。SpringCache帮我们封装了Caffeine pom文件引入
  1. org.springframework.boot
  2. spring-boot-starter-cache
  3. com.github.ben-manes.caffeine
  4. caffeine
  5. 2.6.0

第一种方式

首先配置一个Cache,通过构造者模式构建一个Cache对象,然后后续关于缓存的增删查都是基于这个cache对象。

  1. @Configuration
  2. publicclassCacheConfig{
  3. @Bean
  4. publicCachecaffeineCache(){
  5. returnCaffeine.newBuilder()
  6. //设置最后一次写入或访问后经过固定时间过期
  7. .expireAfterWrite(60,TimeUnit.SECONDS)
  8. //初始的缓存空间大小
  9. .initialCapacity(100)
  10. //缓存的最大条数
  11. .maximumSize(1000)
  12. .build();
  13. }

第一种方式我们就一一不介绍了,基本上就是使用caffeineCache来根据你自己的业务来操作以下方法

SpringBoot 如何快速使用 Caffeine 缓存?

这种方式使用的话是对代码有侵入性的。

第二种方式

  • 需要在SpingBoot启动类标上EnableCaching注解,这个玩意跟很多框架都一样,比如我们肴集成dubbo也需要标上@EnableDubbole注解等。
  1. @SpringBootApplication
  2. @EnableCaching
  3. publicclassDemoApplication{
  4. publicstaticvoidmain(String[]args){
  5. SpringApplication.run(DemoApplication.class,args);
  6. }
  • 在application.yml配置我们的使用的缓存类型、过期时间、缓存策略等。
  1. spring:
  2. profiles:
  3. active:dev
  4. cache:
  5. type:CAFFEINE
  6. caffeine:
  7. spec:maximumSize=500,expireAfterAccess=600s

如果我们不习惯使用这种方式的配置,当然我们也可以使用JavaConfig的配置方式来代替配置文件。

  1. @Configuration
  2. publicclassCacheConfig{
  3. @Bean
  4. publicCacheManagercacheManager(){
  5. CaffeineCacheManagercacheManager=newCaffeineCacheManager();
  6. cacheManager.setCaffeine(Caffeine.newBuilder()
  7. //设置最后一次写入或访问后经过固定时间过期
  8. .expireAfterAccess(600,TimeUnit.SECONDS)
  9. //初始的缓存空间大小
  10. .initialCapacity(100)
  11. //缓存的最大条数
  12. .maximumSize(500));
  13. returncacheManager;
  14. }

接下来就是代码中如何来使用这个缓存了。

  1. @Override
  2. @CachePut(value="user",key="#userDTO.id")
  3. publicUserDTOsave(UserDTOuserDTO){
  4. userRepository.save(userDTO);
  5. returnuserDTO;
  6. }
  7. @Override
  8. @CacheEvict(value="user",key="#id")//2
  9. publicvoidremove(Longid){
  10. logger.info("删除了id、key为"+id+"的数据缓存");
  11. }
  12. @Override
  13. @Cacheable(value="user",key="#id")
  14. publicUserDTOgetUserById(Longid){
  15. returnuserRepository.findOne(id);
  16. }

上述代码中我们可以看到有几个注解@CachePut、@CacheEvict、@Cacheable我们只需要在方法上标上这几个注解,我们就能够使用缓存了,我们分别来介绍下这几个注解。

@Cacheable

@Cacheable它是既可以标注在类上也可以标注在方法上,当它标记在类上的时候它表述这个类上面的所有方法都会支持缓存,同样的 当它作用在法上面时候它表示这个方法是支持缓存的。比如上面我们代码中的getUserById这个方法第一次缓存里面没有数据,我们会去查询DB,但是第二次来查询的时候就不会走DB查询了,而是直接从缓存里面拿到结果就返回了。

value 属性

  • @Cacheable的value属性是必须指定的,其表示当前方法的返回值是会被缓存在哪个Cache上的,对应Cache的名称。

key

  • @Cacheable的key 有两种方式一种是我们自己显示的去指定我们的key,还有一种默认的生成策略,默认的生成策略是SimpleKeyGenerator这个类,这个生成key的方式也比较简单我们可以看下它的源码:
  1. publicstaticObjectgenerateKey(Object...params){
  2. //如果方法没有参数key就是一个newSimpleKey()
  3. if(params.length==0){
  4. returnSimpleKey.EMPTY;
  5. }
  6. //如果方法只有一个参数key就是当前参数
  7. if(params.length==1){
  8. Objectparam=params[0];
  9. if(param!=null&&!param.getClass().isArray()){
  10. returnparam;
  11. }
  12. }
  13. //如果key是多个参数,key就是newSimpleKey,不过这个SimpleKey对象的hashCode和Equals方法是根据方法传入的参数重写的。
  14. returnnewSimpleKey(params);
  15. }

上述代码还是非常好理解的分为三种情况:

  • 方法没有参数,那就new使用一个全局空的SimpleKey对象来作为key。
  • 方法就一个参数,就使用当前参数来作为key
  • 方法参数大于1个,就new一个SimpleKey对象来作为key,new 这个SimpleKey的时候用传入的参数重写了SimpleKey的hashCode和equals方法, 至于为啥需要重写的原因话,就跟Map用自定义对象来作为key的时候必须要重写hashCode和equals方法原理是一样的,因为caffein也是借助了ConcurrentHashMap来实现,

小结

上述代码我们可以发现默认生成key只跟我们传入的参数有关系,如果我们有一个类里面如果存在多个没有参数的方法,然后我们使用了默认的缓存生成策略的话,就会造成缓存丢失。或者缓存相互覆盖,或者还有可能会发生ClassCastException 因为都是使用同一个key。比如下面这代码就会发生异常(ClassCastException)。

  1. @Cacheable(value="user")
  2. publicUserDTOgetUser(){
  3. UserDTOuserDTO=newUserDTO();
  4. userDTO.setUserName("Java金融");
  5. returnuserDTO;
  6. }
  7. @Cacheable(value="user")
  8. publicUserDTO2getUser1(){
  9. UserDTO2userDTO2=newUserDTO2();
  10. userDTO2.setUserName2("javajr.cn");
  11. returnuserDTO2;
  12. }

所以一般不怎么推荐使用默认的缓存生成key的策略。如果非要用的话我们最好自己重写一下,带上方法名字等。类似于如下代码:

  1. @Component
  2. publicclassMyKeyGeneratorextendsSimpleKeyGenerator{
  3. @Override
  4. publicObjectgenerate(Objecttarget,Methodmethod,Object...params){
  5. Objectgenerate=super.generate(target,method,params);
  6. Stringformat=MessageFormat.format("{0}{1}{2}",method.toGenericString(),generate);
  7. returnformat;
  8. }

自定义key

我们可以通过Spring的EL表达式来指定我们的key。这里的EL表达式可以使用方法参数及它们对应的属性。使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”这也是我们比较推荐的做法:

  1. @Cacheable(value="user",key="#id")
  2. publicUserDTOgetUserById(Longid){
  3. UserDTOuserDTO=newUserDTO();
  4. userDTO.setUserName("java金融");
  5. returnuserDTO;
  6. }
  7. @Cacheable(value="user",key="#p0")
  8. publicUserDTOgetUserById1(Longid){
  9. returnnull;
  10. }
  11. @Cacheable(value="user",key="#userDTO.id")
  12. publicUserDTOgetUserById2(UserDTOuserDTO){
  13. returnnull;
  14. }
  15. @Cacheable(value="user",key="#p0.id")
  16. publicUserDTOgetUserById3(UserDTOuserDTO){
  17. returnnull;
  18. }

@CachePut

@CachePut指定的属性是和@Cacheable一样的,但是它们两个是有区别的,@CachePut标注的方法不会先去查询缓存是否有值,而是每次都会先去执行该方法,然后把结果返回,并且结果也会缓存起来。

SpringBoot 如何快速使用 Caffeine 缓存?

为什么是这样的一个流程我们可以去看看它的源码关键代码就是这一行,

  1. Cache.ValueWrappercacheHit=findCachedItem(contexts.get(CacheableOperation.class));

当我们使用方法上有@Cacheable注解的时候再contexts里面会把CacheableOperation加入进去,只有contexts.get(CacheableOperation.class)取到的内容不为空的话,才会去从缓存里面取内容,否则的话cacheHit会直接返回null。至于contexts什么时候加入CacheableOperation的话我们看下SpringCacheAnnotationParser#parseCacheAnnotations这个方法就会明白的。具体的源码就不展示了,感兴趣的可以自己去翻。

@CacheEvict

把缓存中数据删除,用法跟前面两个注解差不多有value和key属性,需要注意一点的是它多了一个属性beforeInvocation

  • beforeInvocation 这个属性需要注意下它的默认值是false,false代表的意思是再执调用方法之前不删除缓存,只有方法执行成功之后才会去删除缓存。设置为true的话调用方法之前会去删除一下缓存,方法执行成功之后还会去调用删除缓存这样就是双删了。如果方法执行异常的话就不会去删除缓存。
  • allEntrie 是否清空所有缓存内容,默认值为 false,如果指定为 true,则方法调用后将立即清空所有缓存

@Caching

这是一个组合注解集成了上面三个注解,有三个属性:cacheable、put和evict,分别用于来指定@Cacheable、@CachePut和@CacheEvict。

小结

第二种方式是侵入式的,它的实现原理也比较简单就是通过切面的方法拦截器来实现,拦截所有的方法,它的核心代码如下:看起来就跟我们的业务代码差不了多少,感兴趣的也可以去瞅一瞅。

  1. if(contexts.isSynchronized()){
  2. CacheOperationContextcontext=contexts.get(CacheableOperation.class).iterator().next();
  3. if(isConditionPassing(context,CacheOperationExpressionEvaluator.NO_RESULT)){
  4. Objectkey=generateKey(context,CacheOperationExpressionEvaluator.NO_RESULT);
  5. Cachecache=context.getCaches().iterator().next();
  6. try{
  7. returnwrapCacheValue(method,cache.get(key,()->unwrapReturnValue(invokeOperation(invoker))));
  8. }
  9. catch(Cache.ValueRetrievalExceptionex){
  10. //TheinvokerwrapsanyThrowableinaThrowableWrapperinstancesowe
  11. //canjustmakesurethatonebubblesupthestack.
  12. throw(CacheOperationInvoker.ThrowableWrapper)ex.getCause();
  13. }
  14. }
  15. else{
  16. //Nocachingrequired,onlycalltheunderlyingmethod
  17. returninvokeOperation(invoker);
  18. }
  19. }
  20. //Processanyearlyevictions
  21. //beforeInvocation属性是否为true,如果是true就删除缓存
  22. processCacheEvicts(contexts.get(CacheEvictOperation.class),true,
  23. CacheOperationExpressionEvaluator.NO_RESULT);
  24. //Checkifwehaveacacheditemmatchingtheconditions
  25. Cache.ValueWrappercacheHit=findCachedItem(contexts.get(CacheableOperation.class));
  26. //Collectputsfromany@Cacheablemiss,ifnocacheditemisfound
  27. ListcachePutRequests=newLinkedList<>();
  28. if(cacheHit==null){
  29. collectPutRequests(contexts.get(CacheableOperation.class),
  30. CacheOperationExpressionEvaluator.NO_RESULT,cachePutRequests);
  31. }
  32. ObjectcacheValue;
  33. ObjectreturnValue;
  34. if(cacheHit!=null&&!hasCachePut(contexts)){
  35. //Iftherearenoputrequests,justusethecachehit
  36. cacheValue=cacheHit.get();
  37. returnValue=wrapCacheValue(method,cacheValue);
  38. }
  39. else{
  40. //Invokethemethodifwedon'thaveacachehit
  41. returnValue=invokeOperation(invoker);
  42. cacheValue=unwrapReturnValue(returnValue);
  43. }
  44. //Collectanyexplicit@CachePuts
  45. collectPutRequests(contexts.get(CachePutOperation.class),cacheValue,cachePutRequests);
  46. //Processanycollectedputrequests,eitherfrom@CachePutora@Cacheablemiss
  47. for(CachePutRequestcachePutRequest:cachePutRequests){
  48. cachePutRequest.apply(cacheValue);
  49. }
  50. //Processanylateevictions
  51. processCacheEvicts(contexts.get(CacheEvictOperation.class),false,cacheValue);
  52. returnreturnValue;
  53. }

结束

由于自己才疏学浅,难免会有纰漏,假如你发现了错误的地方,还望留言给我指出来,我会对其加以修正。

感谢您的阅读,十分欢迎并感谢您的关注。

站在巨人的肩膀上摘苹果: https://www.cnblogs.com/fashflying/p/6908028.html#!comments

原文链接:https://mp.weixin.qq.com/s/A-Vqojp6IJRIfwTSCqbZ-A