大家好,我是大都督周瑜,今天这篇给大家分析一下Spring Cache中最核心的源码,我相信一定会对大家工作和面试有很大帮助。
欢迎大家关注我的个人公众号:IT周瑜,公众号中有我个人整理的高质量免费技术资料、知识脑图、精品面试题
在看源码之前,务必先了解Spring Cache的多种用法,同时在看源码时,建议打开IDEA一边看文章,一边看源码。
@Cacheable
使用方式:
@Cacheable(value = "base", key = "#key")
@GetMapping("/get")
public String getValue(@RequestParam(required = false) String key) {
return "v1";
}
@Cacheable的作用是先检查缓存中是否存在指定的key,如果存在则返回缓存值,如果不存在则执行方法,并将方法结执行果Put到缓存中。
其中@Cacheable中的value属性表示key的前缀,最终在redis中的key为:
127.0.0.1:6379> keys *
1) "base::k3"
2) "base::k2"
3) "base::k1"
@CachePut
@CachePut的作用是将指定key和方法返回结果Put到缓存中,所以肯定是先执行方法,再Put到缓存。
@CachePut(value = "base", key = "#key")
@GetMapping("/put")
public String putValue(String key) {
return "zhouyu";
}
@CacheEvict
@CacheEvict的作用是删除缓存中的指定key。
@CacheEvict(value = "base", key = "#key")
@GetMapping("/remove")
public void removeValue(String key) {
// 方法逻辑
}
那缓存删除是在方法执行之前还是之后执行呢?默认是之后,也就是先执行方法,再删除缓存,可以通过@CacheEvict注解的beforeInvocation
属性来进行控制。
比如以下代码表示会在方法执行之前进行缓存删除,先就是先删缓存,再执行方法
@CacheEvict(value = "base", key = "#key", beforeInvocation = true)
@GetMapping("/remove")
public void removeValue(String key) {
// 方法逻辑
}
另外,@CacheEvict中还有一个属性allEntries,默认为false,表示只删除指定key,如果设置为true,则表示会删除base前缀的所有key。
@Caching
对于@Cacheable、CachePut、@CacheEvict在默认情况下是可以用在同一个方法上的,比如:
@Cacheable(value = "base", key = "k1")
@CachePut(value = "base", key = "k2")
@CacheEvict(value = "base", key = "k3")
@GetMapping("/get")
public String getValue() {
return "zhouyu";
}
表示当执行这个方法时,会从缓存中获取k1的value,也会将k2和方法执行结果Put到缓存中,同时还会删除k3。
但是,同一个注解只能在一个方法上用一次,比如以下写法是不支持的,编译就会报错:
@CacheEvict(value = "base", key = "k3")
@CacheEvict(value = "base", key = "k4")
@GetMapping("/get")
public String getValue() {
return "zhouyu";
}
而Spring Cache为了支持这种情况,提供了一个@Caching注解,比如以下代码表示执行方法时会删除多个key:
@Caching(evict = {@CacheEvict(value = "base", key = "k3"),
@CacheEvict(value = "base", key = "k4")})
@GetMapping("/get")
public String getValue() {
return "zhouyu";
}
同样@Caching也支持多个@CachePut操作,表示执行方法时将方法结果同时Put到多个key中。
当然@Caching也支持多个@Cacheable,那难道执行方法时获取多个key的值?那不是会获取出来多个值吗?用哪一个呢?卖个关子,我们留着这个疑问来看看源码是如何处理的吧。
核心源码分析
实现以上注解对应功能的核心类为CacheInterceptor,核心方法为execute()方法,我们来依次看看源码的实现。
非同步模式
首先源码中会判断是否为同步模式,默认非同步,这段我们先放放,后面再来单独解析。
// 第一段:处理@Cacheable的同步模式,默认非同步
if (contexts.isSynchronized()) {
...
return;
}
// 非同步模式
如果是非同步模式,也就是默认情况下会执行以下逻辑,我先给出主要的几个步骤:
- 执行beforeInvocation属性为false的缓存删除操作
- 检查缓存是否存在
- 如果缓存不存在,则执行方法,得到方法返回结果,并将结果Put到缓存中
- 如果缓存存在,但方法上没有定义@CachePut,最终方法返回的就是缓存值
- 如果缓存存在,但方法上定义了@CachePut,则需要执行方法,并将方法返回结果Put到缓存中,最终返回的也是方法执行结果(因为有Put操作,执行完Put操作后,方法返回结果和缓存值是一样的)
- 执行beforeInvocation属性为true的缓存删除操作
以上这些步骤并不是每个方法都会执行,主要看方法上定义了哪些注解,比如只有定义了@CacheEvict注解才会执行缓存删除相关的操作。
接下来,我们来详细的分析一下源码实现。
首先获取当前执行方法上的@CacheEvict注解,但是需要注意,@CacheEvict注解有一个beforeInvocation属性,默认为false,表示缓存删除是在当前方法执行之前进行删除,还是方法执行之后进行删除。
所以,此时获取的是beforeInvocation属性为true的的@CacheEvict注解,如果存在则会执行缓存删除操作,源码为:
// Process any early evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class),
true,
CacheOperationExpressionEvaluator.NO_RESULT);
源码中的contexts我们可以理解为当前方法的上下文,因为一个方法上既可以定义@Cacheable,也可以同时定义@CachePut、@CacheEvict,你甚至可以通过@Caching注解一次性定义多个@CachePut、@CacheEvict等操作,相当于一个方法既可以获取缓存,也可以删除缓存,也可以直接设置缓存。
因此contexts.get(CacheEvictOperation.class)
表示获取当前方法上的@CacheEvict注解,也就是定义的缓存删除操作,而第二个参数就是beforeInvocation,传入的是true,表示获取需要在方法执行之前进行的缓存删除操作,第三个参数是用来进行条件判断的,可以不用管。
而在processCacheEvicts()方法中就会遍历并执行缓存删除操作,源码如下:
private void processCacheEvicts(
Collection<CacheOperationContext> contexts, boolean beforeInvocation, @Nullable Object result) {
for (CacheOperationContext context : contexts) {
CacheEvictOperation operation = (CacheEvictOperation) context.metadata.operation;
if (beforeInvocation == operation.isBeforeInvocation() && isConditionPassing(context, result)) {
// 执行缓存删除操作
// 内部会判断allEntries属性
performCacheEvict(context, operation, result);
}
}
}
因此,这一部分所做的就是在方法执行之前删除指定key对应的缓存。
紧接着的源码如下:
// Check if we have a cached value matching the conditions
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
大家应该能看懂,就是从当前方法上获取@Cacheable注解,并获取指定key在缓存中对应的value,相当于检查缓存是否命中,那如果一个方法上有多个@Cacheable注解呢?请看源码:
private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) {
Object result = CacheOperationExpressionEvaluator.NO_RESULT;
// 依次遍历@Cacheable
for (CacheOperationContext context : contexts) {
if (isConditionPassing(context, result)) {
Object key = generateKey(context, result);
Cache.ValueWrapper cached = findInCaches(context, key);
// 缓存命中就直接返回
if (cached != null) {
return cached;
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No cache entry for key '" + key + "' in cache(s) " + context.getCacheNames());
}
}
}
}
return null;
}
因此前面留下的疑问对应的答案为:依次遍历@Cacheable注解,有一个命中就直接返回。
因此,这一部分所做的就是处理@Cacheable注解,并检查缓存是否命中。
那缓存命中和缓存没有命中会如何处理呢,接着看源码:
// Collect puts from any @Cacheable miss, if no cached value is found
List<CachePutRequest> cachePutRequests = new ArrayList<>(1);
if (cacheHit == null) {
collectPutRequests(contexts.get(CacheableOperation.class),
CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
}
cacheHit == null
表示缓存没有命中,按@Cacheable注解的功能,缓存如果没有命中则应该执行方法,得到方法结果,并设置到缓存中,因此在缓存没有命中的情况下,@Cacheable注解就相当于一个@CachePut注解,也需要Put值到缓存中。
因此以上代码就是在收集需要执行的Put操作,为什么只收集,但不执行Put操作呢?因为方法还没有执行呀,Put啥到缓存呢,因此这里只是先记录,后续得到方法返回结果后才会Put。
那紧接着应该就要执行方法了吧,我们看源码:
Object cacheValue;
Object returnValue;
// 如果缓存存在并且没有定义Put操作,就不需要执行方法了,直接返回缓存中的值
if (cacheHit != null && !hasCachePut(contexts)) {
// If there are no put requests, just use the cache hit
cacheValue = cacheHit.get();
returnValue = wrapCacheValue(method, cacheValue);
}
else {
// 如果缓存没有命中或定义了Put操作,则需要执行方法
// Invoke the method if we don't have a cache hit
returnValue = invokeOperation(invoker);
cacheValue = unwrapReturnValue(returnValue);
}
先理解hasCachePut()
,这个方法是在检查当前所执行的方法上是否有Put操作,有同学可能会想,刚刚不是已经收集了吗,注意刚刚收集的是@Cacheable,而hasCachePut()
判断的是@CachePut的Put操作。
从源码上看,分以下情况:
- 缓存命中+没有定义Put操作:不需要执行方法,直接返回缓存中的值
- 缓存命中+定义了Put操作:需要执行方法,尽管缓存命中了,但Put操作希望的是把方法执行结果设置到缓存中
- 缓存没有命中:那就必须执行方法得到方法返回结果,并设置到缓存中
只不过以上代码还没有真正将方法返回结果Put到缓存中,紧接着下面的源码就会进行Put了:
// 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);
}
这段代码好理解了,其实就是把@CachePut注解对应的Put操作也收集起来放到cachePutRequests集合中,之前的@Cacheable注解对应的Put操作也在这个集合中,因此以上代码的for循环就是在执行所有的Put操作,而cacheValue就是方法执行结果。
最后,再处理一下beforeInvocation
属性为false的情况,表示执行那些需要在方法执行之后执行的缓存删除操作:
// Process any late evictions
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
以上就是非同步模式下的执行流程,我们回头再看看前面总结的流程:
- 执行beforeInvocation属性为false的缓存删除操作
- 检查缓存是否存在
- 如果缓存不存在,则执行方法,得到方法返回结果,并将结果Put到缓存中
- 如果缓存存在,但方法上没有定义@CachePut,最终方法返回的就是缓存值
- 如果缓存存在,但方法上定义了@CachePut,则需要执行方法,并将方法返回结果Put到缓存中,最终返回的也是方法执行结果(因为有Put操作,执行完Put操作后,方法返回结果和缓存值是一样的)
- 执行beforeInvocation属性为true的缓存删除操作
同步模式
以上是非同步模式下的执行流程,接下来分析同步模式,其实同步模式比较简单。
首先,只有@Cacheable注解才有sync
属性,也就是同步模式的开关,其他注解是没有这个属性的。
而且如果开启了同步模式,那么其他注解是不会生效的,比如以下代码在执行时会报错:
@Cacheable(value = "base", key = "k1", sync = true)
@CachePut(value = "base", key = "k2")
@GetMapping("/get")
public String getValue() {
return "zhouyu";
}
错误为:
A sync=true operation cannot be combined with other cache operations...
对应源码为:
if (syncEnabled) {
if (this.contexts.size() > 1) {
throw new IllegalStateException(
"A sync=true operation cannot be combined with other cache operations on '" + method + "'");
}
...
return true;
}
那同步模式的作用是什么呢?
我们再来理解@Cacheable注解的作用:会先查询缓存,如果命中则直接返回,如果没有命中则执行方法,将方法返回值缓存。
特别是在没有命中的情况下,是存在并发安全问题的,假如有两个线程,A和B,线程A执行方法结果为R1,线程B执行方法结果为R2,当两个线程同时执行时,最终缓存中的结果是不确定的,线程A以为缓存中存的值是R1,但实际可能为R2。
因此,所谓的同步模式,其实就是保证@Cacheable注解对应操作的原子性,保证@Cacheable注解的并发安全。
那如何实现的呢?我们看源码:
if (contexts.isSynchronized()) {
// 只获取定义的@Cacheable注解
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 {
// 重点是执行handleSynchronizedGet方法
return wrapCacheValue(method, handleSynchronizedGet(invoker, key, cache));
}
catch (Cache.ValueRetrievalException ex) {
...
}
}
else {
// 直接执行方法
return invokeOperation(invoker);
}
}
以上代码的重点是执行handleSynchronizedGet()
方法,我们看它的源码:
@Nullable
private Object handleSynchronizedGet(CacheOperationInvoker invoker, Object key, Cache cache) {
InvocationAwareResult invocationResult = new InvocationAwareResult();
// 从cache中获取指定的key的值,如果不存在则执行lambda表达式
Object result = cache.get(key, () -> {
invocationResult.invoked = true;
if (logger.isTraceEnabled()) {
logger.trace("No cache entry for key '" + key + "' in cache " + cache.getName());
}
return unwrapReturnValue(invokeOperation(invoker));
});
if (!invocationResult.invoked && logger.isTraceEnabled()) {
logger.trace("Cache entry for key '" + key + "' found in cache '" + cache.getName() + "'");
}
return result;
}
Cache
是一个接口,我们现在的缓存是redis,所以现在用的实现类为RedisCache,我们看RedisCache中get的具体实现:
public <T> T get(Object key, Callable<T> valueLoader) {
// 获取值
ValueWrapper result = get(key);
// 存在则返回
if (result != null) {
return (T) result.get();
}
// 不存在则调用getSynchronized()
return getSynchronized(key, valueLoader);
}
继续看getSynchronized()
方法的实现:
// 注意这里用了synchronized关键字,加了锁
private synchronized <T> T getSynchronized(Object key, Callable<T> valueLoader) {
// 加锁之后再次检查缓存中是否有值
ValueWrapper result = get(key);
// 有值则返回
if (result != null) {
return (T) result.get();
}
// 否则则执行valueLoader,对应的就是前面的lambda表达式
T value;
try {
value = valueLoader.call();
} catch (Exception e) {
throw new ValueRetrievalException(key, valueLoader, e);
}
// put到缓存中
put(key, value);
return value;
}
再来看lambda表达式:
Object result = cache.get(key, () -> {
invocationResult.invoked = true;
if (logger.isTraceEnabled()) {
logger.trace("No cache entry for key '" + key + "' in cache " + cache.getName());
}
// 执行方法
return unwrapReturnValue(invokeOperation(invoker));
});
核心就是就是调用invokeOperation()
,也就是执行方法。
因此所谓的同步模式就是:
- 获取缓存中指定key的值
- 如果缓存中存在,则直接返回该值,这种情况不会加锁
- 如果缓存中不存在,则利用synchronized关键字进行加锁
- 加到锁之后,再次检查缓存中是否存在,相当于DCL
- 如果仍然不存在,则执行方法,得到方法返回结果,并put到缓存中,然后释放锁
- 最终返回该方法执行结果
另外,我注意到,RedisCache中只有getSynchronized()
方法前面加了synchronized关键字,其他方法,比如put()、evict()等方法都没有加,所以再次确认同步模式只针对@Cacheable注解,对于单独的缓存更新或删除操作是不会加锁的。
总结
Spring Cache整体而言,就是利用Spring AOP机制,利用代理对象在执行方法时对缓存进行操作,除开以上源码流程外,其实还有比如@EnableCaching的实现源码,以及RedisCache是怎么和Redis进行交互的实现源码,下次再分析吧,
我是大都督周瑜,记得关注我,大家如果有收获,帮忙点赞、分享一下,感谢。