Spring Cache核心源码分析

大家好,我是大都督周瑜,今天这篇给大家分析一下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;
}

// 非同步模式

如果是非同步模式,也就是默认情况下会执行以下逻辑,我先给出主要的几个步骤:

  1. 执行beforeInvocation属性为false的缓存删除操作
  2. 检查缓存是否存在
    1. 如果缓存不存在,则执行方法,得到方法返回结果,并将结果Put到缓存中
    2. 如果缓存存在,但方法上没有定义@CachePut,最终方法返回的就是缓存值
    3. 如果缓存存在,但方法上定义了@CachePut,则需要执行方法,并将方法返回结果Put到缓存中,最终返回的也是方法执行结果(因为有Put操作,执行完Put操作后,方法返回结果和缓存值是一样的)
  3. 执行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操作。

从源码上看,分以下情况:

  1. 缓存命中+没有定义Put操作:不需要执行方法,直接返回缓存中的值
  2. 缓存命中+定义了Put操作:需要执行方法,尽管缓存命中了,但Put操作希望的是把方法执行结果设置到缓存中
  3. 缓存没有命中:那就必须执行方法得到方法返回结果,并设置到缓存中

只不过以上代码还没有真正将方法返回结果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);

以上就是非同步模式下的执行流程,我们回头再看看前面总结的流程:

  1. 执行beforeInvocation属性为false的缓存删除操作
  2. 检查缓存是否存在
    1. 如果缓存不存在,则执行方法,得到方法返回结果,并将结果Put到缓存中
    2. 如果缓存存在,但方法上没有定义@CachePut,最终方法返回的就是缓存值
    3. 如果缓存存在,但方法上定义了@CachePut,则需要执行方法,并将方法返回结果Put到缓存中,最终返回的也是方法执行结果(因为有Put操作,执行完Put操作后,方法返回结果和缓存值是一样的)
  3. 执行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(),也就是执行方法。

因此所谓的同步模式就是:

  1. 获取缓存中指定key的值
  2. 如果缓存中存在,则直接返回该值,这种情况不会加锁
  3. 如果缓存中不存在,则利用synchronized关键字进行加锁
  4. 加到锁之后,再次检查缓存中是否存在,相当于DCL
  5. 如果仍然不存在,则执行方法,得到方法返回结果,并put到缓存中,然后释放锁
  6. 最终返回该方法执行结果

另外,我注意到,RedisCache中只有getSynchronized()方法前面加了synchronized关键字,其他方法,比如put()、evict()等方法都没有加,所以再次确认同步模式只针对@Cacheable注解,对于单独的缓存更新或删除操作是不会加锁的。

总结

Spring Cache整体而言,就是利用Spring AOP机制,利用代理对象在执行方法时对缓存进行操作,除开以上源码流程外,其实还有比如@EnableCaching的实现源码,以及RedisCache是怎么和Redis进行交互的实现源码,下次再分析吧,

我是大都督周瑜,记得关注我,大家如果有收获,帮忙点赞分享一下,感谢。

  • 18
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值