文章目录

  • 1. Spring Cache 回顾
    • 开启SpringCache
    • 核心注解
    • @Cacheable
    • @CachePut
    • @CacheEvict
  • 2. Spring Cache 源码初探
    • 2.1 开启缓存 - @EnableCaching
    • 2.2 默认的配置类 - ProxyCachingConfiguration
      • 2.2.1 CacheOperationSource
      • 2.2.2 CacheInterceptor
      • 2.2.3 BeanFactoryCacheOperationSourceAdvisor
    • 2.3 SpringBoot中的自动配置
    • 2.4 CacheInterceptor 核心
      • 2.4.1 CacheOperationContexts 的封装
      • 2.4.2 execute 执行模板
      • 2.4.3 Optional支持
    • 2.5 Spring Cache 的 EL
    • 2.6 Spring Cache 的事务支持
  • 3. Spring Cache 常用实现类
    • 3.1 Cache 接口详解
    • 3.2 RedisCacheManger - SpringBoot 1.5.3 版本
      • 3.2.1 RedisCacheManger 配置项
      • 3.2.2 Cache 实现
      • 3.2.3 通过源码看坑
    • 3.3 RedisCacheManger - SpringBoot 2.3.x 版本
    • 3.4 RedissonCacheManager 实现
  • 4. 文档参考

​ 基于版本 Spring 5.2.12.RELEASE、 Spring Boot 2.3.7.RELEASE

1. Spring Cache 回顾

开启SpringCache

@Configuration
@EnableCaching
public class AppConfig {}
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:cache="http://www.springframework.org/schema/cache"xsi:schemaLocation="http://www.springframework.org/schema/beanshttps://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache.xsd"><cache:annotation-driven/>
</beans>

核心注解

  • @Cacheable: 触发缓存填充
  • @CacheEvict: 触发缓存清除
  • @CachePut: 在不干扰方法执行情况下更新缓存
  • @Caching: 再一个方法上应用多个缓存操作
  • @CacheConfig: 定义在类上共享一些缓存设置

@Cacheable

@Cacheable(cacheNames={"activity", "activityCache2"}, key="#activity.activityCode")
public Activity findActivity(Activity activity)@Cacheable(cacheNames="activity", key="T(com.hzins.activity.enum.ActivityTypeEnum).of(#p0)")
public List<Activity> findActivity(Byte activityType)

keyGenerator

//  keyGenerator 和 key 是互斥的
@Cacheable(cacheNames={"activity"}, keyGenerator="myKeyGenerator")
public Activity findActivity(Activity activity, UserInfo optUser)public interface KeyGenerator {Object generate(Object target, Method method, Object... params);
}// 默认实现类 SimpleKeyGenerator

缓存选择

// cacheManager cacheResolver 互斥
@Cacheable(cacheNames="books", cacheManager="guavaCacheManager")
public Book findBook(ISBN isbn) {...}@Cacheable(cacheResolver="myCacheResolver")
public Book findBook(ISBN isbn) {...}public interface CacheManager {Cache getCache(String name);Collection<String> getCacheNames();
}public interface CacheResolver {Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context);
}

缓存同步

@Cacheable(cacheNames="foos", sync=true)
public Foo executeExpensiveOperation(String id) {...}

条件缓存

// condition执行业务代码前求值,unless在方法执行后求值
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback")
public Book findBook(String name) {...}// 注意 #result 不是Optional,而是里面的对象,所以需要 ?. 判空
@Cacheable(cacheNames="book", unless="#result?.hardback")
public Optional<Book> findBook(String name) {...}

SpEL 上下文

#root.methodName
#root.method
#root.target
#root.targetClass
#root.args[0]
#root.caches[0].name
#argumentName #a0 #p0
#result

@CachePut

q:为什么官网强烈不建议@CachePut和@Cacheable一起使用?
a:因为put操作会强制执行业务代码并更新缓存

@CacheEvict

// beforeInvocation 代表执行方法前就清空缓存
@CacheEvict(cacheNames="books", allEntries=true, beforeInvocation=true)
public void loadBooks(InputStream batch)

2. Spring Cache 源码初探

2.1 开启缓存 - @EnableCaching

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CachingConfigurationSelector.class)
public @interface EnableCaching {/*** 当mode设置为PROXY时有效,为true时启用 CGLIB (subclass-based),默认false使用 JDK 代理(interface-based)*/boolean proxyTargetClass() default false;/*** 默认为代理PROXY模式,也支持AdviceMode.ASPECTJ* 注意PROXY模式通用问题:同一实例中本地调用不生效(this.xxMethod())*/AdviceMode mode() default AdviceMode.PROXY;int order() default Ordered.LOWEST_PRECEDENCE;
}

CachingConfigurationSelector

/*** 根据 @EnableCache中设置的mode来判断最后使用 ProxyCachingConfiguration 或者 AspectJCachingConfiguration* 同时和 JCache 相关的支持*/
public String[] selectImports(AdviceMode adviceMode) {switch (adviceMode) {case PROXY:return getProxyImports();case ASPECTJ:return getAspectJImports();default:return null;}
}
private String[] getProxyImports() {List<String> result = new ArrayList<>(3);// 向容器注册一个自动代理创建器 APCresult.add(AutoProxyRegistrar.class.getName());// 核心配置类result.add(ProxyCachingConfiguration.class.getName());if (jsr107Present && jcacheImplPresent) {result.add(PROXY_JCACHE_CONFIGURATION_CLASS);}return StringUtils.toStringArray(result);
}

2.2 默认的配置类 - ProxyCachingConfiguration

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyCachingConfiguration extends AbstractCachingConfiguration {@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)@Role(BeanDefinition.ROLE_INFRASTRUCTURE)public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor() {BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();advisor.setCacheOperationSource(cacheOperationSource());advisor.setAdvice(cacheInterceptor());if (this.enableCaching != null) {advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));}return advisor;}@Bean@Role(BeanDefinition.ROLE_INFRASTRUCTURE)public CacheOperationSource cacheOperationSource() {return new AnnotationCacheOperationSource();}@Bean@Role(BeanDefinition.ROLE_INFRASTRUCTURE)public CacheInterceptor cacheInterceptor() {CacheInterceptor interceptor = new CacheInterceptor();interceptor.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager);interceptor.setCacheOperationSource(cacheOperationSource());return interceptor;}
}

我们来一个个分析,按照 cacheOperationSource()、cacheInterceptor()、cacheAdvisor() 循序来分析

2.2.1 CacheOperationSource

/*** CacheAspectSupport 中使用到的接口,实现知道如何从 配置、元数据属性或其它地方获取缓存操作属性*/
public interface CacheOperationSource {/*** 5.2开始的优化接口,实现需要校验类中是否有用到SpringCache,如果没有返回false。* fasle时将每个method将不会调用getCacheOperations自省(introspection),提升了启动性能* 相关类可参考 CacheOperationSourcePointcut#matches、AbstractAutoProxyCreator#postProcessAfterInitialization*/default boolean isCandidateClass(Class<?> targetClass) {return true;}/*** 在调用方法时CacheInterceptor拦截,获取当前method包含的缓存相关操作*/Collection<CacheOperation> getCacheOperations(Method method, @Nullable Class<?> targetClass);
}

通过IDE工具打开继承关系,发现以下一些实现

NameMatchCacheOperationSource

​ 用于支持xml配置的cache

CompositeCacheOperationSource

​ 用于组合多个CacheOperationSource实现

AbstractFallbackCacheOperationSource

​ 提供默认的getCacheOperations实现模版:1.对结果做了缓存 2.从method、class提取缓存操作

​ 它申明了2个接口需要子类实现

protected abstract Collection<CacheOperation> findCacheOperations(Class<?> clazz);protected abstract Collection<CacheOperation> findCacheOperations(Method method);
private Collection<CacheOperation> computeCacheOperations(Method method, @Nullable Class<?> targetClass) {// 判断是否是publicif (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {return null;}// 原始方法可能是接口,获取实现类的方法Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);// 先尝试从方法上提取缓存操作Collection<CacheOperation> opDef = findCacheOperations(specificMethod);if (opDef != null) {return opDef;}// 如果没有再尝试从类上提取opDef = findCacheOperations(specificMethod.getDeclaringClass());if (opDef != null && ClassUtils.isUserLevelMethod(method)) {return opDef;}if (specificMethod != method) {// 降级从原始方法获取opDef = findCacheOperations(method);if (opDef != null) {return opDef;}// 最后从原始类上获取opDef = findCacheOperations(method.getDeclaringClass());if (opDef != null && ClassUtils.isUserLevelMethod(method)) {return opDef;}}return null;}

AnnotationCacheOperationSource

​ 它实现的这2个接口均是通过CacheAnnotationParser处理,Spring内置的实现类是 SpringCacheAnnotationParser,核心代码如下

private Collection<CacheOperation> parseCacheAnnotations(DefaultCacheConfig cachingConfig, AnnotatedElement ae) {Collection<CacheOperation> ops = parseCacheAnnotations(cachingConfig, ae, false);if (ops != null && ops.size() > 1) {// localOps相当于只查询类上定义的方法,覆盖接口上定义的方法Collection<CacheOperation> localOps = parseCacheAnnotations(cachingConfig, ae, true);if (localOps != null) {return localOps;}}return ops;
}

2.2.2 CacheInterceptor

 @Bean@Role(BeanDefinition.ROLE_INFRASTRUCTURE)public CacheInterceptor cacheInterceptor() {CacheInterceptor interceptor = new CacheInterceptor();// 设置默认的4个组件interceptor.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager);interceptor.setCacheOperationSource(cacheOperationSource());return interceptor;}

观察ProxyCachingConfiguration的父类 AbstractCachingConfiguration,它通过注入一个CachingConfigurer来配置默认的errorHandler、keyGenerator、cacheResolver、cacheManager

@Configuration
public abstract class AbstractCachingConfiguration implements ImportAware {@Autowired(required = false)void setConfigurers(Collection<CachingConfigurer> configurers) {if (CollectionUtils.isEmpty(configurers)) {return;}if (configurers.size() > 1) {throw new IllegalStateException(configurers.size() + "xxx");}CachingConfigurer configurer = configurers.iterator().next();useCachingConfigurer(configurer);}protected void useCachingConfigurer(CachingConfigurer config) {this.cacheManager = config::cacheManager;this.cacheResolver = config::cacheResolver;this.keyGenerator = config::keyGenerator;this.errorHandler = config::errorHandler;}
}

​ 如果没有时,在 interceptor.configure 方法内会设置默认类 SimpleCacheErrorHandler(抛出异常)、SimpleKeyGenerator(将方法参数包装成SimpleKey对象作为缓存key,相当于根据入参的equals和hashcode判断是否为同一个key)、SimpleCacheResolver(获取CacheOperation中的cacheNames,然后通过cacheManager查询对应的cache)


2.2.3 BeanFactoryCacheOperationSourceAdvisor

​ BeanFactoryCacheOperationSourceAdvisor 继承自 AbstractBeanFactoryPointcutAdvisor,其 pointcut 使用的是固定的 CacheOperationSourcePointcut。内部的 CacheOperationSourcePointcut 的代码如下

@Override
public boolean matches(Method method, Class<?> targetClass) {CacheOperationSource cas = getCacheOperationSource();return (cas != null && !CollectionUtils.isEmpty(cas.getCacheOperations(method, targetClass)));
}

​ 意味着pointcat就是所有能提取出CacheOperation的方法

2.3 SpringBoot中的自动配置

​ 在 spring-boot-autoconfigure 的srping.factories文件中将 CacheAutoConfiguration 设置为自动装配类

static class CacheConfigurationImportSelector implements ImportSelector {@Overridepublic String[] selectImports(AnnotationMetadata importingClassMetadata) {CacheType[] types = CacheType.values();String[] imports = new String[types.length];for (int i = 0; i < types.length; i++) {imports[i] = CacheConfigurations.getConfigurationClass(types[i]);}return imports;}
}

Spring Boot在2.x版本提供的实现类如下

static {Map<CacheType, Class<?>> mappings = new EnumMap<>(CacheType.class);mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class);mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class);mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class);mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class);mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class);mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class);mappings.put(CacheType.REDIS, RedisCacheConfiguration.class);// 在1.5.x版本中是 GuavaCache,2.x后用 Caffeine 替代了mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class);mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class);mappings.put(CacheType.NONE, NoOpCacheConfiguration.class);MAPPINGS = Collections.unmodifiableMap(mappings);
}

​ 这些Configuration类主要配置了对应的 CacheManager bean,通过并且如果没有配置默认的CacheManager,就会在CacheAspectSupport#afterSingletonsInstantiated 时将之设置为默认。

​ 通过代码 @Conditional(CacheCondition.class) ,读CacheCondition代码可知道,如果配置了 spring.cache.type 则优先选用对应的实现,否则就依照上述顺序选择第一个满足条件的配置类。

@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {// ...BindResult<CacheType> specified = Binder.get(environment).bind("spring.cache.type", CacheType.class);if (!specified.isBound()) {return ConditionOutcome.match(message.because("automatic cache type"));}CacheType required = CacheConfigurations.getType(((AnnotationMetadata) metadata).getClassName());if (specified.get() == required) {return ConditionOutcome.match(message.because(specified.get() + " cache type"));}// ...return ConditionOutcome.noMatch(message.because("unknown cache type"));
}

如果项目中需要配置多个 CacheManager,则需要手动配置相关的 @Bean,且需要把默认的CacheManager设置为 @Primary ,或者存在一个CachingConfigurer的bean来设置默认的cacheManager

2.4 CacheInterceptor 核心

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kbU8Kqyn-1619015801058)(.\SpringCache源码解析.assets\image-20210407111248169.png)]

protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {if (this.initialized) {Class<?> targetClass = getTargetClass(target);// 这个就是在ProxyCachingConfiguration中配置的AnnotationCacheOperationSourceCacheOperationSource cacheOperationSource = getCacheOperationSource();if (cacheOperationSource != null) {// 提取缓存操作Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);if (!CollectionUtils.isEmpty(operations)) {return execute(invoker, method,new CacheOperationContexts(operations, method, args, target, targetClass));}}}return invoker.invoke();
}

2.4.1 CacheOperationContexts 的封装

public CacheOperationContexts(Collection<? extends CacheOperation> operations, Method method,Object[] args, Object target, Class<?> targetClass) {this.contexts = new LinkedMultiValueMap<>(operations.size());for (CacheOperation op : operations) {// 相当于对cache分组,并转换成contextthis.contexts.add(op.getClass(), getOperationContext(op, method, args, target, targetClass));}// sync判断和校验this.sync = determineSyncFlag(method);
}protected CacheOperationContext getOperationContext(CacheOperation operation, Method method, Object[] args, Object target, Class<?> targetClass) {CacheOperationMetadata metadata = getCacheOperationMetadata(operation, method, targetClass);return new CacheOperationContext(metadata, args, target);
}

​ CacheOperationMetadata 的创建代码

protected CacheOperationMetadata getCacheOperationMetadata(CacheOperation operation, Method method, Class<?> targetClass) {// 每个带缓存操作的方法都会有一个metadata,且在第一次操作后缓存起来CacheOperationCacheKey cacheKey = new CacheOperationCacheKey(operation, method, targetClass);CacheOperationMetadata metadata = this.metadataCache.get(cacheKey);if (metadata == null) {KeyGenerator operationKeyGenerator;if (StringUtils.hasText(operation.getKeyGenerator())) {// 提取定义在缓存注解上的 keyGeneratoroperationKeyGenerator = getBean(operation.getKeyGenerator(), KeyGenerator.class);} else {// 获取默认的生成器,如果没指定,使用 SimpleKeyGeneratoroperationKeyGenerator = getKeyGenerator();}CacheResolver operationCacheResolver;if (StringUtils.hasText(operation.getCacheResolver())) {// 优先使用缓存注解上指定的 CacheResolveroperationCacheResolver = getBean(operation.getCacheResolver(), CacheResolver.class);} else if (StringUtils.hasText(operation.getCacheManager())) {// 缓存注解上指定的 CacheManager 并且用设置进 SimpleCacheResolverCacheManager cacheManager = getBean(operation.getCacheManager(), CacheManager.class);operationCacheResolver = new SimpleCacheResolver(cacheManager);} else {operationCacheResolver = getCacheResolver();Assert.state(operationCacheResolver != null, "No CacheResolver/CacheManager set");}metadata = new CacheOperationMetadata(operation, method, targetClass, operationKeyGenerator, operationCacheResolver);this.metadataCache.put(cacheKey, metadata);}return metadata;
}

​ SimpleCacheResolver 的作用是从 context 中拿到缓存操作,并拿到上面声明的 cacheNames,然后调用设置进去的 cacheManager 的 getCache 方法拿到对应的Cache对象

2.4.2 execute 执行模板

@Nullable
private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {// 声明 sync = true 执行的代码逻辑if (contexts.isSynchronized()) {// 因为在CacheOperationContexts时,校验了此时只能有一个缓存操作,所以只取了第一个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, handleSynchronizedGet(invoker, key, cache));} catch (Cache.ValueRetrievalException ex) {ReflectionUtils.rethrowRuntimeException(ex.getCause());}} else {return invokeOperation(invoker);}}// 处理 @CacheEvict 设置了beforeInvocation=trueprocessCacheEvicts(contexts.get(CacheEvictOperation.class), true, CacheOperationExpressionEvaluator.NO_RESULT);// 处理@Cacheable,从缓存查询对应keyCache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));List<CachePutRequest> cachePutRequests = new LinkedList<>();if (cacheHit == null) {//处理@Cacheable,如果没有命中缓存,则生成cachePutRequests,用作后续执行collectPutRequests(contexts.get(CacheableOperation.class),CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);}Object cacheValue;Object returnValue;// 如果@Cacheable命中缓存,且没有@CachePut的操作,则用缓存的值// ps. 解释了官网描述不建议@CachePut和@Cacheable一起使用if (cacheHit != null && !hasCachePut(contexts)) {cacheValue = cacheHit.get();returnValue = wrapCacheValue(method, cacheValue);} else {// 执行业务代码returnValue = invokeOperation(invoker);cacheValue = unwrapReturnValue(returnValue);}// 提取 @CachePut 操作collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);// 执行@CachePut 或 @Cacheable miss后的写缓存操作for (CachePutRequest cachePutRequest : cachePutRequests) {cachePutRequest.apply(cacheValue);}// @CacheEvict 后置清除缓存processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);return returnValue;
}

2.4.3 Optional支持

通过下面的代码,可提取Optional中的内容存到缓存,和读取缓存内容返回Optional格式

public static Object unwrapOptional(@Nullable Object obj) {if (obj instanceof Optional) {Optional<?> optional = (Optional<?>) obj;if (!optional.isPresent()) {return null;}Object result = optional.get();Assert.isTrue(!(result instanceof Optional), "Multi-level Optional usage not supported");return result;}return obj;
}@Nullable
private Object wrapCacheValue(Method method, @Nullable Object cacheValue) {if (method.getReturnType() == Optional.class &&(cacheValue == null || cacheValue.getClass() != Optional.class)) {return Optional.ofNullable(cacheValue);}return cacheValue;
}

2.5 Spring Cache 的 EL

核心类 CacheOperationExpressionEvaluator,它继承了 CachedExpressionEvaluator

基类中定义了一个内部类 ExpressionKey,它包含 AnnotatedElementKey 和 一个 expression(String类型)

同时默认构造器里使用了 SpelExpressionParser 作为EL的解析器,其中关键代码为:

protected Expression getExpression(Map<ExpressionKey, Expression> cache,AnnotatedElementKey elementKey, String expression) {ExpressionKey expressionKey = createKey(elementKey, expression);Expression expr = cache.get(expressionKey);if (expr == null) {expr = getParser().parseExpression(expression);cache.put(expressionKey, expr);}return expr;
}

以及默认的默认的 ParameterNameDiscoverer 提供 el 中 “#参数名” 的设置

private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();public DefaultParameterNameDiscoverer() {// 虽说设置了多个,但是获取时代码控制只有第一个 discoverer 的getParameterNames返回有结果就不执行后面别的 discovererif (KotlinDetector.isKotlinReflectPresent() && !GraalDetector.inImageCode()) {addDiscoverer(new KotlinReflectionParameterNameDiscoverer());}// 1.8后有的,基于反射addDiscoverer(new StandardReflectionParameterNameDiscoverer());// 基于 ASMaddDiscoverer(new LocalVariableTableParameterNameDiscoverer());
}

CacheOperationExpressionEvaluator ,createEvaluationContext 方法提供了 “#result” 的设置

同时在返回的 CacheEvaluationContext 里,对注解中condition条件时加入的 #result 访问屏蔽。


在来观察context的上下文基类 MethodBasedEvaluationContext,此方法提供了参数变量的el支持 (#a0,#p0…)

protected void lazyLoadArguments() {if (ObjectUtils.isEmpty(this.arguments)) {return;}// 调用上文所述的parameterNameDiscoverer解析参数名String[] paramNames = this.parameterNameDiscoverer.getParameterNames(this.method);int paramCount = (paramNames != null ? paramNames.length : this.method.getParameterCount());int argsCount = this.arguments.length;for (int i = 0; i < paramCount; i++) {Object value = null;if (argsCount > paramCount && i == paramCount - 1) {// 将剩余的参数作为一个数组value = Arrays.copyOfRange(this.arguments, i, argsCount);} else if (argsCount > i) {value = this.arguments[i];}setVariable("a" + i, value);setVariable("p" + i, value);if (paramNames != null && paramNames[i] != null) {setVariable(paramNames[i], value);}}
}

继续回到 CacheOperationExpressionEvaluator ,针对 key、condition、unless 的 el 分别用不同的缓存调用基类的方法创建表达式

@Nullable
public Object key(String keyExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) {return getExpression(this.keyCache, methodKey, keyExpression).getValue(evalContext);
}public boolean condition(String conditionExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) {return (Boolean.TRUE.equals(getExpression(this.conditionCache, methodKey, conditionExpression).getValue(evalContext, Boolean.class)));
}public boolean unless(String unlessExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext){return (Boolean.TRUE.equals(getExpression(this.unlessCache, methodKey, unlessExpression).getValue(evalContext, Boolean.class)));
}

2.6 Spring Cache 的事务支持

AbstractTransactionSupportingCacheManager ,提供了支持 Spring 事务的 缓存管理器。

需要手动调用 setTransactionAware(true) 时才能激活 put/evict 操作被spring事务管理,最终在事务提交后才执行 put/evict 操作

常用的如 RedisCacheManager、EhCacheCacheManager 都继承了它。

protected Cache decorateCache(Cache cache) {// 在 AbstactCacheManager 中所有拿到的 Cache 均会调用此方法,开启事务同步后对 Cache 进行包装return (isTransactionAware() ? new TransactionAwareCacheDecorator(cache) : cache);
}

TransactionAwareCacheDecorator 核心代码为

public void put(final Object key, @Nullable final Object value) {if (TransactionSynchronizationManager.isSynchronizationActive()) {TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {@Overridepublic void afterCommit() {TransactionAwareCacheDecorator.this.targetCache.put(key, value);}});} else {this.targetCache.put(key, value);}
}public void evict(final Object key) {if (TransactionSynchronizationManager.isSynchronizationActive()) {TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {@Overridepublic void afterCommit() {TransactionAwareCacheDecorator.this.targetCache.evict(key);}});} else {this.targetCache.evict(key);}
}

3. Spring Cache 常用实现类

3.1 Cache 接口详解

/*** 注意需要支持存储返回的null值*/
public interface Cache {String getName();/*** 返回cache实现对象,如concurentHashMap、RedisCacheWriter、caffeine.Cache*/Object getNativeCache();/*** 获取指定key的缓存数据,用ValueWrapper包装,包括对应结果为null对象** 直接返回null意味着缓存不包含该key的映射*/@NullableValueWrapper get(Object key);/*** 返回强转后的对象** 返回null没法区分是本身结果null,还是不包含该key的映射** @throws: IllegalStateException 类型不匹配时抛出*/@Nullable<T> T get(Object key, @Nullable Class<T> type);/*** 这个方法需要实现此逻辑:if cached, return; otherwise create, cache and return* 同时需要尽可能保证操作是同步的,以便于并发调用时,也只执行一次 valueLoader** 此方法为声明 sync=true 时会调用*/@Nullable<T> T get(Object key, Callable<T> valueLoader);/*** 允许实现可以延迟、异步*/void put(Object key, @Nullable Object value);/*** 允许实现类可以非原子方式分为2步实现*/@Nullabledefault ValueWrapper putIfAbsent(Object key, @Nullable Object value) {ValueWrapper existingValue = get(key);if (existingValue == null) {put(key, value);}return existingValue;}/*** 允许实现可异步、延迟*/void evict(Object key);/*** 最好调用后能立即清除对应缓存*/default boolean evictIfPresent(Object key) {evict(key);return false;}/*** 清除所有的缓存,允许实现异步、延迟*/void clear();/*** 最好调用后能立即清除所有缓存*/default boolean invalidate() {clear();return false;}
}

最好回到 CacheAspectSupport#execute 中,观察Spring中每种操作调用的 Cache 方法加深理解

3.2 RedisCacheManger - SpringBoot 1.5.3 版本

因为公司项目中用的spring boot 为 1.5.3,所以单独分析一下, 对应 Spring Data Redis版本 1.7.2.RELEASE

3.2.1 RedisCacheManger 配置项

  • redisOperations

    使用的 RedisTemplate,可以通过扩展序列化工具,支持自定义格式的缓存格式或者开启压缩

  • usePrefix、cachePrefix

    usePrefix设置为true时(默认为false),在 createCache 时,会调用 cachePrefix.prefix(cacheName) 方法设置前缀,默认的前缀为 cacheName:

    如果没有前缀,会用一个 **cacheName + “~keys” ** 的 sortedset 维护添加的key,在调用清空缓存操作时,利用它来清空所有的缓存

    // RedisCacheCleanByKeysCallbackpublic Void doInLock(RedisConnection connection) {int offset = 0;boolean finished = false;do {Set<byte[]> keys = connection.zRange(metadata.getSetOfKnownKeysKey(), (offset) * PAGE_SIZE, (offset + 1) * PAGE_SIZE - 1);finished = keys.size() < PAGE_SIZE;offset++;if (!keys.isEmpty()) {connection.del(keys.toArray(new byte[keys.size()][]));}} while (!finished);connection.del(metadata.getSetOfKnownKeysKey());return null;
    }
    

    如果设置了前缀,清空时是使用了 keys 命令查询匹配的 KeyPrefix(默认是cacheName:) + “*” 来清除

    RedisCacheCleanByPrefixCallbackpublic Void doInLock(RedisConnection connection) throws DataAccessException {// 拼接key的bytebyte[] prefixToUse = Arrays.copyOf(metadata.getKeyPrefix(), metadata.getKeyPrefix().length + WILD_CARD.length);System.arraycopy(WILD_CARD, 0, prefixToUse, metadata.getKeyPrefix().length, WILD_CARD.length);if(isClusterConnection(connection)) {// Cluster不支持lua,所以执行keys然后删除Set<byte[]> keys = connection.keys(prefixToUse);if (!keys.isEmpty()) {connection.del(keys.toArray(new byte[keys.size()][]));}} else {// local keys = redis.call('KEYS', ARGV[1]); local keysCount = table.getn(keys); if(keysCount > 0) then for _, key in ipairs(keys) do redis.call('del', key); end; end; return keysCount;connection.eval(REMOVE_KEYS_BY_PATTERN_LUA, ReturnType.INTEGER, 0, prefixToUse);}return null;
    }
    
  • loadRemoteCachesOnStartup

    默认为fasle,当设置为true时,会在启动后通过拉取远端的 *~keys ,来找出所有的 cacheName,并且提前创建 RedisCache

    protected Set<String> loadRemoteCacheKeys() {return (Set<String>) redisOperations.execute(new RedisCallback<Set<String>>() {@Overridepublic Set<String> doInRedis(RedisConnection connection) throws DataAccessException {Set<byte[]> keys = connection.keys(redisOperations.getKeySerializer().serialize("*~keys"));Set<String> cacheKeys = new LinkedHashSet<String>();if (!CollectionUtils.isEmpty(keys)) {for (byte[] key : keys) {cacheKeys.add(redisOperations.getKeySerializer().deserialize(key).toString().replace("~keys", ""));}}return cacheKeys;}});
    }
    
  • dynamic、configuredCacheNames

    如果 RedisCacheManager 设置了 configuredCacheNames (通过构造方法或setCacheNames设置),dynamic会变为false

    此时,RedisCacheManager 不在会自动创建未设置到configuredCacheNames 中的 Cache,如配置了

    spring.cache.cache-names=cache1,cache2
    

    执行以下方法时将报错 Cannot find cache named cache3 for xxx

    @Cacheable(cacheNames="cache3", cacheManager="redisCacheManager")
    public Object getById(Integer id);
    
  • defaultExpiration、expires

    在创建 RedisCache 前,会调用以下方法设置过期时间

    protected long computeExpiration(String name) {Long expiration = null;if (expires != null) {expiration = expires.get(name);}return (expiration != null ? expiration.longValue() : defaultExpiration);
    }
    

    defaultExpiration 即全局默认过期时间,默认0等于不过期;expires 配置指定 cacheName 的过期时间

3.2.2 Cache 实现

​ 这个版本中,所有的命令执行前,都需要检查当前不存在 lock;在 clear 和 处理 get(key, valueLoader) 时会加锁


ValueWrapper get(Object key);

判断有无lock,如果有,一直300ms间隔检查指导它不存在,才执行get

注意在SpringBoot1.5.6,也就是Spring Data Redis 1.8 的版本中,get方法实现方式:
先exist查询key是否存在,若存在需要判断当前是否有 ~lock,有锁时每300毫秒再查询锁是否存在,直到不存在后才执行 get
不仅效率问题,而且还存在了因exist后,缓存过期,拿到了一个null值的问题


T get(Object key, Callable valueLoader);

先同get逻辑一样,如果没有,加载valueLoader执行逻辑

public byte[] doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException {try {// 等待锁和加锁,注意这不是原子操作,所以下面的代码会尽可能的弥补并发的情况lock(connection);try {// double checkbyte[] value = connection.get(element.getKeyBytes());if (value != null) {return value;}if (!isClusterConnection(connection)) {// redis非cluster架构,启用transactions// https://redis.io/topics/transactionsconnection.watch(element.getKeyBytes());connection.multi();}// 执行了callback代码,拿到返回结果value = element.get();if (value.length == 0) {connection.del(element.getKeyBytes());} else {connection.set(element.getKeyBytes(), value);// 设置过期时间processKeyExpiration(element, connection);// 如果没有keyPrefix,则使用一个zset管理key,并对zset设置过期时间maintainKnownKeys(element, connection);}if (!isClusterConnection(connection)) {connection.exec();}return value;} catch (RuntimeException e) {if (!isClusterConnection(connection)) {connection.discard();}throw e;}} finally {unlock(connection);}}
};

void put(Object key, @Nullable Object value);

​ put 操作对比上面,不会加锁(当然任然需要等待锁),也不会开启 watch 命令


ValueWrapper putIfAbsent(Object key, @Nullable Object value);

​ 使用setNX命令实现


void evict(Object key);

​ 删除key后,如果没有 keyPrefix,则还会删除 zset中的数据


void clear();

​ 如果有 keyPrefix,且非cluster,使用lua删除,否则执行keys keyPrefix* 命令删除

​ 没有 keyPrefix 则通过 zset 中的数据来删除

​ 注意,以上两种方式,都需要设置锁后才能进行


3.2.3 通过源码看坑

1.  lock key 没过期时间,且waitForLock又没超时,遇上必炸2.  接上,sync=true,看似解决==缓存击穿==,实则加大了因程序重启导致lock key未被删除的风险3.  @CacheEvict(allEntries=true) 和 sync=true时,会严重阻塞住当前 cacheName 下的所有读写操作4.  注意spring boot 1.5.6 下的版本存在高并发下 get 方法可能返回 null 的 bug5.  不可重入6.  当从用不失效调整为带过期时间后,有的缓存key将永远无法被更新

3.3 RedisCacheManger - SpringBoot 2.3.x 版本

新版本中( Spring Data Redis 2.0版本后),它将更注重性能的提升。

默认为non-lock模式使用,忽略了lock的校验。且不再使用zset管理key,来达到优化性能的目的

设置 RedisCacheWriter#lockingRedisCacheWriter 、 RedisCacheWriter#nonLockingRedisCacheWriter

ValueWrapper get(Object key);

​ 没有了老版本的 exist 校验操作,如果是lock模式需要校验锁


T get(Object key, Callable valueLoader);

​ 此操作加了 synchronized,而不再对全局加lock key,就算是lock模式下也只会校验当前存在lock key 时需要等待;

​ 且不使用transactions了,也不使用zset管理key


void put(Object key, @Nullable Object value);

lock模式会校验锁,直接set key


ValueWrapper putIfAbsent(Object key, @Nullable Object value);

​ lock模式下会检查锁,且也会设置锁(lock key还是没有过期时间,好在源码中不调用此方法,建议别用)


void evict(Object key);

​ lock模式才会检查锁,然后直接delete


void clear();

​ lock模式时才会加锁,使用keys * 的方式删除。所以,请一定记得确保有前缀(默认开启),有前缀时删除的带前缀的key


总结

  1. 此版本的实现更注重性能,其实大多数场景下使用 none-lock模式也是可以的
  2. lock key无过期时间,没有采用过期时间+续约的方式设置,还是会有坑,不过sync=true总算是敢开启了
  3. lock模式还是不支持可重入

3.4 RedissonCacheManager 实现

后续解读,作者水平有限,看不懂。应该比SpringData实现稍慢,但一致性要更好

https://gitee.com/mirrors/redisson/wikis/14.-%E7%AC%AC%E4%B8%89%E6%96%B9%E6%A1%86%E6%9E%B6%E6%95%B4%E5%90%88#142-spring-cache%E6%95%B4%E5%90%88

4. 文档参考

https://docs.spring.io/spring-framework/docs/5.2.12.RELEASE/spring-framework-reference/integration.html#cache

https://docs.spring.io/spring-boot/docs/2.3.7.RELEASE/reference/htmlsingle/#boot-features-caching

https://docs.spring.io/spring-data/redis/docs/current/changelog.txt

SpringCache源码学习笔记相关推荐

  1. Java多线程之JUC包:Semaphore源码学习笔记

    若有不正之处请多多谅解,并欢迎批评指正. 请尊重作者劳动成果,转载请标明原文链接: http://www.cnblogs.com/go2sea/p/5625536.html Semaphore是JUC ...

  2. RocketMQ 源码学习笔记 Producer 是怎么将消息发送至 Broker 的?

    RocketMQ 源码学习笔记 Producer 是怎么将消息发送至 Broker 的? 文章目录 RocketMQ 源码学习笔记 Producer 是怎么将消息发送至 Broker 的? 前言 项目 ...

  3. Vuex 4源码学习笔记 - 通过Vuex源码学习E2E测试(十一)

    在上一篇笔记中:Vuex 4源码学习笔记 - 做好changelog更新日志很重要(十) 我们学到了通过conventional-changelog来生成项目的Changelog更新日志,通过更新日志 ...

  4. Vuex 4源码学习笔记 - Vuex是怎么与Vue结合?(三)

    在上一篇笔记中:Vuex源码学习笔记 - Vuex开发运行流程(二) 我们通过运行npm run dev命令来启动webpack,来开发Vuex,并在Vuex的createStore函数中添加了第一个 ...

  5. jquery源码学习笔记三:jQuery工厂剖析

    jquery源码学习笔记二:jQuery工厂 jquery源码学习笔记一:总体结构 上两篇说过,query的核心是一个jQuery工厂.其代码如下 function( window, noGlobal ...

  6. 雷神FFMpeg源码学习笔记

    雷神FFMpeg源码学习笔记 文章目录 雷神FFMpeg源码学习笔记 读取编码并依据编码初始化内容结构 每一帧的视频解码处理 读取编码并依据编码初始化内容结构 在开始编解码视频的时候首先第一步需要注册 ...

  7. Apache log4j-1.2.17源码学习笔记

    (1)Apache log4j-1.2.17源码学习笔记 http://blog.csdn.net/zilong_zilong/article/details/78715500 (2)Apache l ...

  8. PHP Yac cache 源码学习笔记

    YAC 源码学习笔记 本文地址 http://blog.csdn.net/fanhengguang_php/article/details/54863955 config.m4 检测系统共享内存支持情 ...

  9. Vuex 4源码学习笔记 - 通过dispatch一步步来掌握Vuex整个数据流(五)

    在上一篇笔记中:Vuex 4源码学习笔记 - Store 构造函数都干了什么(四) 我们通过查看Store 构造函数的源代码可以看到主要做了三件事情: 初始化一些内部变量以外 执行installMod ...

最新文章

  1. word 数组 转 指针_Word之VBA丨文档中的图片怎样批量加边框?
  2. 《因果科学周刊》第6期:领域自适应
  3. 一分钟学会spring注解之@Scope注解
  4. SAE 的极致应用部署效率
  5. checkbox设置三种状态 qt_checkbox的三种状态处理
  6. androidstudio build tools安装_Android Studio4.0 安装及配置
  7. 技术人员,该如何向业务和产品“砍需求”?
  8. no suitable driver found for jdbc:mysql//localhost:3306/..
  9. python基础-基础知识(包括:函数递归等知识)
  10. 分布式系统架构实战demo:SSM+Dubbo
  11. angular $location服务获取url
  12. cc2530单片机是几位单片机_我的单片机学习之路(续1)
  13. 地下迷宫探索 (30 分)(DFS)
  14. Spring Cloud 微服务架构图
  15. 划重点!划重点!2022面试必刷461道大厂架构面试真题汇总+面经+简历模板
  16. 面试算法高频压轴题——灯泡开关问题
  17. Lexar雷克沙 nCARD存储卡全网发布,可用于华为手机内存扩容
  18. 双向可控硅的触发电路设计
  19. EMC电磁兼容测试服务包括
  20. 基础工资提高至35万美元、带薪病假天数翻倍,亚马逊、苹果为留人才又出新动作

热门文章

  1. 领歌敏捷协作——设置微信接收卡片提醒
  2. 2020-11-30 脑残记录
  3. 建模助手『 一键参数』快速修改构件,族参数
  4. 基于NodeJs的爬虫
  5. 盘点数据挖掘中常见的5种 AutoEDA 工具
  6. Linux系统运维之keepalived的工作原理和裂脑
  7. Git Commit emoji Guide
  8. 2017-2018年QS世界大学计算机科学及信息系统专业排名
  9. sqoop1连接数据库踩的大坑
  10. 解决:‘vue-cli-service‘ 不是内部或外部命令,也不是可运行的程序 或批处理文件