热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

深入理解SpringCache框架

今天给大家分析一下Spring框架本身对这些缓存具体实现的支持和融合。使用SpringCache将大大的减少我们的Spring项目中缓存使用的复杂度,提高代码可读性。本文将从以下几个方面来认识SpringCache框架。感兴趣的小伙伴们可以参考一下

本文是缓存系列第三篇,前两篇分别介绍了 Guava 和 JetCache。

前两篇我们讲了 Guava 和 JetCache,它们都是缓存的具体实现,今天给大家分析一下 Spring 框架本身对这些缓存具体实现的支持和融合。使用 Spring Cache 将大大的减少我们的Spring项目中缓存使用的复杂度,提高代码可读性。本文将从以下几个方面来认识Spring Cache框架。

背景

SpringCache 产生的背景其实与Spring产生的背景有点类似。由于 Java EE 系统框架臃肿、低效,代码可观性低,对象创建和依赖关系复杂, Spring 框架出来了,目前基本上所有的Java后台项目都离不开 Spring 或 SpringBoot (对 Spring 的进一步简化)。现在项目面临高并发的问题越来越多,各类缓存的应用也增多,那么在通用的 Spring 框架上,就需要有一种更加便捷简单的方式,来完成缓存的支持,就这样 SpringCache就出现了。

不过首先我们需要明白的一点是,SpringCache 并非某一种 Cache 实现的技术,SpringCache 是一种缓存实现的通用技术,基于 Spring 提供的 Cache 框架,让开发者更容易将自己的缓存实现高效便捷的嵌入到自己的项目中。当然,SpringCache 也提供了本身的简单实现 NoOpCacheManager、ConcurrentMapCacheManager 等。通过 SpringCache,可以快速嵌入自己的Cache实现。

用法

源码已分享至Github: https://github.com/zhuzhenke/common-caches

注意点:

1、开启 EnableCaching 注解,默认没有开启 Cache。

2、配置 CacheManager。

@Bean
@Qualifier("concurrentMapCacheManager")
@Primary
ConcurrentMapCacheManager concurrentMapCacheManager() {
  return new ConcurrentMapCacheManager();
}

这里使用了 @Primary 和 @Qualifier 注解,@Qualifier 注解是给这个 Bean 加一个名字,用于同一个接口 Bean 的多个实现时,指定当前 Bean 的名字,也就意味着 CacheManager 可以配置多个,并且在不同的方法场景下使用。@Primary 注解是当接口 Bean 有多个时,优先注入当前 Bean 。

现在拿 CategoryService 实现来分析。

public class CategoryService {

  @Caching(evict = {@CacheEvict(value = CategoryCacheConstants.CATEGORY_DOMAIN,
      key = "#category.getCategoryCacheKey()",
      beforeInvocation = true)})
  public int add(Category category) {
    System.out.println("模拟进行数据库交互操作......");
    System.out.println("Cache became invalid,value:" + CategoryCacheConstants.CATEGORY_DOMAIN
        + ",key:" + category.getCategoryCacheKey());
    return 1;
  }

  @Caching(evict = {@CacheEvict(value = CategoryCacheConstants.CATEGORY_DOMAIN,
      key = "#category.getCategoryCacheKey()",
      beforeInvocation = true)})
  public int delete(Category category) {
    System.out.println("模拟进行数据库交互操作......");
    System.out.println("Cache became invalid,value:" + CategoryCacheConstants.CATEGORY_DOMAIN
        + ",key:" + category.getCategoryCacheKey());
    return 0;
  }

  @Caching(evict = {@CacheEvict(value = CategoryCacheConstants.CATEGORY_DOMAIN,
      key = "#category.getCategoryCacheKey()")})
  public int update(Category category) {
    System.out.println("模拟进行数据库交互操作......");
    System.out.println("Cache updated,value:" + CategoryCacheConstants.CATEGORY_DOMAIN
        + ",key:" + category.getCategoryCacheKey()
        + ",category:" + category);
    return 1;
  }

  @Cacheable(value = CategoryCacheConstants.CATEGORY_DOMAIN,
      key = "#category.getCategoryCacheKey()")
  public Category get(Category category) {
    System.out.println("模拟进行数据库交互操作......");
    Category result = new Category();
    result.setCateId(category.getCateId());
    result.setCateName(category.getCateId() + "CateName");
    result.setParentId(category.getCateId() - 10);
    return result;
  }
}

CategoryService 通过对 category 对象的数据库增删改查,模拟缓存失效和缓存增加的结果。使用非常简便,把注解加在方法上,则可以达到缓存的生效和失效方案。

深入源码

源码分析我们分为几个方面一步一步解释其中的实现原理和实现细节。源码基于 Spring 4.3.7.RELEASE 分析。

发现

SpringCache 在方法上使用注解发挥缓存的作用,缓存的发现是基于 AOP 的 PointCut 和 MethodMatcher 通过在注入的 class 中找到每个方法上的注解,并解析出来。

首先看到 org.springframework.cache.annotation.SpringCacheAnnotationParser 类:

protected Collection parseCacheAnnotations(DefaultCacheConfig cachingConfig, AnnotatedElement ae) {
 Collection ops = null;

 Collection cacheables = AnnotatedElementUtils.getAllMergedAnnotations(ae, Cacheable.class);
 if (!cacheables.isEmpty()) {
 ops = lazyInit(ops);
 for (Cacheable cacheable : cacheables) {
  ops.add(parseCacheableAnnotation(ae, cachingConfig, cacheable));
 }
 }
 Collection evicts = AnnotatedElementUtils.getAllMergedAnnotations(ae, CacheEvict.class);
 if (!evicts.isEmpty()) {
 ops = lazyInit(ops);
 for (CacheEvict evict : evicts) {
  ops.add(parseEvictAnnotation(ae, cachingConfig, evict));
 }
 }
 Collection puts = AnnotatedElementUtils.getAllMergedAnnotations(ae, CachePut.class);
 if (!puts.isEmpty()) {
 ops = lazyInit(ops);
 for (CachePut put : puts) {
  ops.add(parsePutAnnotation(ae, cachingConfig, put));
 }
 }
 Collection cachings = AnnotatedElementUtils.getAllMergedAnnotations(ae, Caching.class);
 if (!cachings.isEmpty()) {
 ops = lazyInit(ops);
 for (Caching caching : cachings) {
  Collection cachingOps = parseCachingAnnotation(ae, cachingConfig, caching);
  if (cachingOps != null) {
  ops.addAll(cachingOps);
  }
 }
 }

 return ops;
}

这个方法会解析 Cacheable、CacheEvict、CachePut 和 Caching 4个注解,找到方法上的这4个注解后,会将注解中的参数解析出来,作为后续注解生效的一个依据。这里举例说一下 CacheEvict 注解。

CacheEvictOperation parseEvictAnnotation(AnnotatedElement ae, DefaultCacheConfig defaultConfig, CacheEvict cacheEvict) {
 CacheEvictOperation.Builder builder = new CacheEvictOperation.Builder();

 builder.setName(ae.toString());
 builder.setCacheNames(cacheEvict.cacheNames());
 builder.setCondition(cacheEvict.condition());
 builder.setKey(cacheEvict.key());
 builder.setKeyGenerator(cacheEvict.keyGenerator());
 builder.setCacheManager(cacheEvict.cacheManager());
 builder.setCacheResolver(cacheEvict.cacheResolver());
 builder.setCacheWide(cacheEvict.allEntries());
 builder.setBeforeInvocation(cacheEvict.beforeInvocation());

 defaultConfig.applyDefault(builder);
 CacheEvictOperation op = builder.build();
 validateCacheOperation(ae, op);

 return op;
}

CacheEvict 注解是用于缓存失效。这里代码会根据 CacheEvict 的配置生产一个 CacheEvictOperation 的类,注解上的 name、key、cacheManager 和 beforeInvocation 等都会传递进来。

另外需要将一下 Caching 注解,这个注解通过 parseCachingAnnotation 方法解析参数,会拆分成 Cacheable、CacheEvict、CachePut 注解,也就对应我们缓存中的增加、失效和更新操作。

Collection parseCachingAnnotation(AnnotatedElement ae, DefaultCacheConfig defaultConfig, Caching caching) {
 Collection ops = null;

 Cacheable[] cacheables = caching.cacheable();
 if (!ObjectUtils.isEmpty(cacheables)) {
 ops = lazyInit(ops);
 for (Cacheable cacheable : cacheables) {
  ops.add(parseCacheableAnnotation(ae, defaultConfig, cacheable));
 }
 }
 CacheEvict[] cacheEvicts = caching.evict();
 if (!ObjectUtils.isEmpty(cacheEvicts)) {
 ops = lazyInit(ops);
 for (CacheEvict cacheEvict : cacheEvicts) {
  ops.add(parseEvictAnnotation(ae, defaultConfig, cacheEvict));
 }
 }
 CachePut[] cachePuts = caching.put();
 if (!ObjectUtils.isEmpty(cachePuts)) {
 ops = lazyInit(ops);
 for (CachePut cachePut : cachePuts) {
  ops.add(parsePutAnnotation(ae, defaultConfig, cachePut));
 }
 }

 return ops;
}

然后回到 AbstractFallbackCacheOperationSource 类:

public Collection getCacheOperations(Method method, Class<&#63;> targetClass) {
 if (method.getDeclaringClass() == Object.class) {
 return null;
 }

 Object cacheKey = getCacheKey(method, targetClass);
 Collection cached = this.attributeCache.get(cacheKey);

 if (cached != null) {
 return (cached != NULL_CACHING_ATTRIBUTE &#63; cached : null);
 }
 else {
 Collection cacheOps = computeCacheOperations(method, targetClass);
 if (cacheOps != null) {
  if (logger.isDebugEnabled()) {
  logger.debug("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheOps);
  }
  this.attributeCache.put(cacheKey, cacheOps);
 }
 else {
  this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE);
 }
 return cacheOps;
 }
}

这里会将解析出来的 CacheOperation 放在当前 Map> attributeCache = new ConcurrentHashMap>(1024); 属性上,为后续拦截方法时处理缓存做好数据的准备。

注解产生作用

当访问 categoryService.get(category) 方法时,会走到 CglibAopProxy.intercept() 方法,这也说明缓存注解是基于动态代理实现,通过方法的拦截来动态设置或失效缓存。方法中会通过 List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); 来拿到当前调用方法的 Interceptor 链。往下走会调用 CacheInterceptor 的 invoke 方法,最终调用 execute 方法,我们重点分析这个方法的实现。

private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
 // Special handling of synchronized invocation
 if (contexts.isSynchronized()) {
 CacheOperationContext cOntext= contexts.get(CacheableOperation.class).iterator().next();
 if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
  Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
  Cache cache = context.getCaches().iterator().next();
  try {
  return wrapCacheValue(method, cache.get(key, new Callable() {
   @Override
   public Object call() throws Exception {
   return unwrapReturnValue(invokeOperation(invoker));
   }
  }));
  }
  catch (Cache.ValueRetrievalException ex) {
  // The invoker wraps any Throwable in a ThrowableWrapper instance so we
  // can just make sure that one bubbles up the stack.
  throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();
  }
 }
 else {
  // No caching required, only call the underlying method
  return invokeOperation(invoker);
 }
 }

 // Process any early evictions
 processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
  CacheOperationExpressionEvaluator.NO_RESULT);

 // Check if we have a cached item matching the conditions
 Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));

 // Collect puts from any @Cacheable miss, if no cached item is found
 List cachePutRequests = new LinkedList();
 if (cacheHit == null) {
 collectPutRequests(contexts.get(CacheableOperation.class),
  CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
 }

 Object cacheValue;
 Object returnValue;

 if (cacheHit != null && cachePutRequests.isEmpty() && !hasCachePut(contexts)) {
 // If there are no put requests, just use the cache hit
 cacheValue = cacheHit.get();
 returnValue = wrapCacheValue(method, cacheValue);
 }
 else {
 // Invoke the method if we don't have a cache hit
 returnValue = invokeOperation(invoker);
 cacheValue = unwrapReturnValue(returnValue);
 }

 // Collect any explicit @CachePuts
 collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);

 // Process any collected put requests, either from @CachePut or a @Cacheable miss
 for (CachePutRequest cachePutRequest : cachePutRequests) {
 cachePutRequest.apply(cacheValue);
 }

 // Process any late evictions
 processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);

 return returnValue;
}

我们的方法没有使用同步,走到 processCacheEvicts 方法。

private void processCacheEvicts(Collection contexts, boolean beforeInvocation, Object result) {
 for (CacheOperationContext context : contexts) {
 CacheEvictOperation operation = (CacheEvictOperation) context.metadata.operation;
 if (beforeInvocation == operation.isBeforeInvocation() && isConditionPassing(context, result)) {
  performCacheEvict(context, operation, result);
 }
 }
}

注意这个方法传入的 beforeInvocation 参数是 true,说明是方法执行前进行的操作,这里是取出 CacheEvictOperation,operation.isBeforeInvocation(),调用下面方法:

private void performCacheEvict(CacheOperationContext context, CacheEvictOperation operation, Object result) {
 Object key = null;
 for (Cache cache : context.getCaches()) {
 if (operation.isCacheWide()) {
  logInvalidating(context, operation, null);
  doClear(cache);
 }
 else {
  if (key == null) {
  key = context.generateKey(result);
  }
  logInvalidating(context, operation, key);
  doEvict(cache, key);
 }
 }
}

这里需要注意了,operation 中有个参数 cacheWide,如果使用这个参数并设置为true,则在缓存失效时,会调用 clear 方法进行全部缓存的清理,否则只对当前 key 进行 evict 操作。本文中,doEvict() 最终会调用到 ConcurrentMapCache的evict(Object key) 方法,将 key 缓存失效。

回到 execute 方法,走到 Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class)); 这一步,这里会根据当前方法是否有 CacheableOperation 注解,进行缓存的查询,如果没有命中缓存,则会调用方法拦截器 CacheInterceptor 的 proceed 方法,进行原方法的调用,得到缓存 key 对应的 value,然后通过 cachePutRequest.apply(cacheValue) 设置缓存。

public void apply(Object result) {
 if (this.context.canPutToCache(result)) {
 for (Cache cache : this.context.getCaches()) {
  doPut(cache, this.key, result);
 }
 }
}

doPut() 方法最终对调用到 ConcurrentMapCache 的 put 方法,完成缓存的设置工作。

最后 execute 方法还有最后一步 processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue); 处理针对执行方法后缓存失效的注解策略。

优缺点

优点

方便快捷高效,可直接嵌入多个现有的 cache 实现,简写了很多代码,可观性非常强。

缺点

  • 内部调用,非 public 方法上使用注解,会导致缓存无效。由于 SpringCache 是基于 Spring AOP 的动态代理实现,由于代理本身的问题,当同一个类中调用另一个方法,会导致另一个方法的缓存不能使用,这个在编码上需要注意,避免在同一个类中这样调用。如果非要这样做,可以通过再次代理调用,如 ((Category)AopContext.currentProxy()).get(category) 这样避免缓存无效。
  • 不能支持多级缓存设置,如默认到本地缓存取数据,本地缓存没有则去远端缓存取数据,然后远程缓存取回来数据再存到本地缓存。

扩展知识点

  • 动态代理:JDK、CGLIB代理。
  • SpringAOP、方法拦截器。

Demo

https://github.com/zhuzhenke/common-caches

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。


推荐阅读
  • 详解 Python 的二元算术运算,为什么说减法只是语法糖?[Python常见问题]
    原题|UnravellingbinaryarithmeticoperationsinPython作者|BrettCannon译者|豌豆花下猫(“Python猫 ... [详细]
  • 在Docker中,将主机目录挂载到容器中作为volume使用时,常常会遇到文件权限问题。这是因为容器内外的UID不同所导致的。本文介绍了解决这个问题的方法,包括使用gosu和suexec工具以及在Dockerfile中配置volume的权限。通过这些方法,可以避免在使用Docker时出现无写权限的情况。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 推荐系统遇上深度学习(十七)详解推荐系统中的常用评测指标
    原创:石晓文小小挖掘机2018-06-18笔者是一个痴迷于挖掘数据中的价值的学习人,希望在平日的工作学习中,挖掘数据的价值, ... [详细]
  • Google Play推出全新的应用内评价API,帮助开发者获取更多优质用户反馈。用户每天在Google Play上发表数百万条评论,这有助于开发者了解用户喜好和改进需求。开发者可以选择在适当的时间请求用户撰写评论,以获得全面而有用的反馈。全新应用内评价功能让用户无需返回应用详情页面即可发表评论,提升用户体验。 ... [详细]
  • FeatureRequestIsyourfeaturerequestrelatedtoaproblem?Please ... [详细]
  • 本文介绍了在go语言中利用(*interface{})(nil)传递参数类型的原理及应用。通过分析Martini框架中的injector类型的声明,解释了values映射表的作用以及parent Injector的含义。同时,讨论了该技术在实际开发中的应用场景。 ... [详细]
  • EPICS Archiver Appliance存储waveform记录的尝试及资源需求分析
    本文介绍了EPICS Archiver Appliance存储waveform记录的尝试过程,并分析了其所需的资源容量。通过解决错误提示和调整内存大小,成功存储了波形数据。然后,讨论了储存环逐束团信号的意义,以及通过记录多圈的束团信号进行参数分析的可能性。波形数据的存储需求巨大,每天需要近250G,一年需要90T。然而,储存环逐束团信号具有重要意义,可以揭示出每个束团的纵向振荡频率和模式。 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 本文介绍了互联网思维中的三个段子,涵盖了餐饮行业、淘品牌和创业企业的案例。通过这些案例,探讨了互联网思维的九大分类和十九条法则。其中包括雕爷牛腩餐厅的成功经验,三只松鼠淘品牌的包装策略以及一家创业企业的销售额增长情况。这些案例展示了互联网思维在不同领域的应用和成功之道。 ... [详细]
  • 从壹开始前后端分离【 .NET Core2.0 +Vue2.0 】框架之六 || API项目整体搭建 6.1 仓储模式
    代码已上传Github+Gitee,文末有地址  书接上文:前几回文章中,我们花了三天的时间简单了解了下接口文档Swagger框架,已经完全解放了我们的以前的Word说明文档,并且可以在线进行调 ... [详细]
  • 弹性云服务器ECS弹性云服务器(ElasticCloudServer)是一种可随时自助获取、可弹性伸缩的云服务器,帮助用户打造可靠、安全、灵活、高效的应用环境 ... [详细]
  • 后台自动化测试与持续部署实践
    后台自动化测试与持续部署实践https:mp.weixin.qq.comslqwGUCKZM0AvEw_xh-7BDA后台自动化测试与持续部署实践原创 腾讯程序员 腾讯技术工程 2 ... [详细]
author-avatar
白白加黑黑99_889
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有