Spring Cache原理解析

 更新时间:2024年05月20日 15:06:52   作者:买断  
Spring Cache是一个框架,它提供了基于注解的缓存功能,使得开发者可以很方便地将缓存集成到他们的应用程序中,这篇文章主要介绍了Spring Cache原理解析,需要的朋友可以参考下

一. 各个注解回顾

在看 Spring Cache 源码时,我们先看下回顾一下几个重要的注解;

1. @Cacheable

在方法执行前查看是否有缓存对应的数据,如果有直接返回数据,如果没有则调用方法获取数据返回,并缓存起来;

  • unless属性:条件符合则不缓存,不符合则缓存;挺容易搞混的;
  • sync属性:是否使用异步,默认是 false;在一个多线程的环境中,某些操作可能被相同的参数并发地调用,同一个 value 值可能被多次计算(或多次访问 db),这样就达不到缓存的目的。针对这些可能高并发的操作,我们可以使用 sync 参数来告诉底层的缓存提供者将缓存的入口锁住,这样就只能有一个线程计算操作的结果值,而其它线程需要等待。当值为true,相当于同步可以有效的避免缓存击穿的问题;
@Cacheable(cacheNames = "cache1", key = "#id")
public User getUserById(int id) {
    User user = userMapper.selectById(id);
    log.info("getUserById: {}", user);
    return user;
}

2. @CachePut

@CachePut ,在每次执行方法时,将执行结果以键值对的形式存入指定缓存中;

@CachePut(cacheNames = "cache1", key = "#user.id", unless = "#result == null")
public User updateUser(User user) {
    int count = userMapper.updateUser(user);
    if (count == 0) {
        log.info("update failed");
        return null;
    }
    return user;
}

3. @CacheEvict

@CacheEvict,Evict,表示驱逐,会在调用方法时从缓存中移除已存储的数据;

@CacheEvict(cacheNames = "cache1", key = "#id")
public int deleteUserById(int id) {
    int result = userMapper.deleteUserById(id);
    log.info("delete result: {}", result);
    return result;
}

@CacheEvict 中有一个属性 beforeInvocation:是否在方法执行前就清空,默认是 false,表示在方法执行后清空缓存;

清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时不会触发清除操作。使用beforeInvocation = true 可以改变触发清除操作的时间,当我们指定该属性值为 true 时,SpringCache 会在调用该方法之前清除缓存中的指定元素;

4. @Caching

可以在一个方法或者类上同时指定多个 SpringCache 相关的注解;

  • 默认情况下不允许同时两个 @CachePut 作用在方法上,此时需要用到 @Caching 进行组合;
  • @CachePut 和 @Cacheable 和 CacheEvict 三者是可以同时作用在方法上的;
@Caching(
    put = {@CachePut(cacheNames = "cache1", key = "#user.id", unless = "#result == null"),
        @CachePut(cacheNames = "cache1", key = "#user.username", unless = "#result == null")}
)
public User updateUser(User user) {
    int count = userMapper.updateUser(user);
    if (count == 0) {
        log.info("update failed");
        return null;
    }
    return user;
}

二. Spring Cache

Spring Cache 注解对应的 PointcutAdvsior 是 BeanFactoryCacheOperationSourceAdvisor,我们主要看它的 Advice,它的 Advice 是 CacheInterceptor;

CacheInterceptor 的处理逻辑和事务相关的 TransactionInterceptor 非常类似,下面我们对 CacheInterceptor 进行简单分析;

1. CacheInterceptor

CacheInterceptor 是一个 Advice,它实现了 MethodInterceptor 接口,我们主要看它作为一个 MethodInterceptor 的 invoke() 逻辑;

// ----------------------------------- CacheInterceptor -------------------------------------
public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor {
    @Override
    public Object invoke(final MethodInvocation invocation) throws Throwable {
        Method method = invocation.getMethod();
        // 采用函数对象的方式,把该函数对象传给父类的 execute() 执行
        // 最终还是执行 invocation.proceed()
        CacheOperationInvoker aopAllianceInvoker = () -> {
            try {
                return invocation.proceed();
            }
            catch (Throwable ex) {
                // 这里对异常做了一次封装,并抛出此异常
                throw new CacheOperationInvoker.ThrowableWrapper(ex);
            }
        };
        Object target = invocation.getThis();
        try {
            // 1. 执行父类的 execute()
            return execute(aopAllianceInvoker, target, method, invocation.getArguments());
        }
        catch (CacheOperationInvoker.ThrowableWrapper th) {
            // 抛出原始异常
            throw th.getOriginal();
        }
    }
}

主要还是执行父类 CacheAspectSupport#execute(),我们看 CacheAspectSupport#execute() 做了啥;

// --------------------------------- CacheAspectSupport -----------------------------------
protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
    if (this.initialized) {
        Class<?> targetClass = getTargetClass(target);
        // 1. 获取 CacheOperationSource
        // 一般我们使用的都是注解类型的,所以会获取到 AnnotationCacheOperationSource
        // AnnotationCacheOperationSource 内部有注解解析器,可以解析得到 CacheOperations
        CacheOperationSource cacheOperationSource = getCacheOperationSource();
        if (cacheOperationSource != null) {
            // 2. 根据 AnnotationCacheOperationSource 解析得到 targetClass#method() 上的 CacheOperations
            // CacheOperation 是一个抽象类,它的三个实现类分别对应 @Cacheable、@CachePut、@CacheEvict
            // CacheableOperation、CachePutOperation、CacheEvictOperation
            // 其实就是解析到目标类目标方法上的注解元信息 CacheOperations
            Collection<CacheOperation> operations = 
                cacheOperationSource.getCacheOperations(method, targetClass);
            if (!CollectionUtils.isEmpty(operations)) {
                // 3. CacheOperations 不为空,将 CacheOperations 等信息聚合为 CacheOperationContexts 对象
                // CacheOperationContexts 类功能很强大,debug 的时候可以点进去看看
                // 每个 CacheOperation 都会对应一个 CacheOperationContext,塞入 CacheOperationContexts
                // 执行重载的 execute()
                return execute(invoker, method,
                     new CacheOperationContexts(operations, method, args, target, targetClass));
            }
        }
    }
    return invoker.invoke();
}

我们看它重载的 execute(),这个是核心方法;

// --------------------------------- CacheAspectSupport -----------------------------------
private Object execute(CacheOperationInvoker invoker, 
                       Method method, 
                       CacheOperationContexts contexts) {
    // 1. 如果方法需要 sync 的话,此处在执行 cache.get() 时会加锁
    // 我们可以先不看此处内部逻辑
    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, handleSynchronizedGet(invoker, key, cache));
            }
            catch (Cache.ValueRetrievalException ex) {
                ReflectionUtils.rethrowRuntimeException(ex.getCause());
            }
        }
        else {
            return invokeOperation(invoker);
        }
    }
    // 2. 执行早期的 CacheEvict
    // 如果 @CacheEvict 的 beforeInvocation 属性是 true 的话(默认是 false)
    // 会在方法执行前就执行 CacheEvict
    processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
                       CacheOperationExpressionEvaluator.NO_RESULT);
    // 3. 如果有 CacheableOperation 的话,也就是有 @Cacheable 注解的话
    // 先去缓存中取缓存值,并包装为 Cache.ValueWrapper 对象
    // 	如果所有的 @Cacheable 都没有缓存值,cacheHit 将为 null
    // 	反过来说,但凡有一个 @Cacheable 有缓存值,cacheHit 将不为 null
    Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
    // 4. 创建一个 List<CachePutRequest> 集合
    // 如果 cacheHit == null,此时需要把所有 @Cacheable 中对应 key 都执行 CachePut 操作
    // 所以这里会收集 CacheableOperation,并作为 CachePutOperation 塞入到 List<CachePutRequest> 中
    List<CachePutRequest> cachePutRequests = new ArrayList<>();
    if (cacheHit == null) {
        collectPutRequests(contexts.get(CacheableOperation.class),
                           CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
    }
    Object cacheValue;
    Object returnValue;
    if (cacheHit != null && !hasCachePut(contexts)) {
        // 5.1 如果缓存中的值不为空,且没有 @CachePut
        // 显然返回值就是 cacheValue,并做一定的包装
        cacheValue = cacheHit.get();
        returnValue = wrapCacheValue(method, cacheValue);
    }
    else {
        // 5.2 其他情况,缓存中的值为空 || 有 @CachePut
        // 执行目标方法,返回值作为新的 cacheValue,并做一定的包装
        // 这里执行目标方法时可能会出现异常,出现异常的情况下就不会执行 CachePut 和 CacheEvict 操作了!!!
        returnValue = invokeOperation(invoker);
        cacheValue = unwrapReturnValue(returnValue);
    }
    // 6. 收集显式声明的 CachePutOperation,也就是有显式声明的 @CachePut
    collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
    // 7. 执行 Cache.put(),处理 CachePut 和没有缓存命中的 Cacheable 
    for (CachePutRequest cachePutRequest : cachePutRequests) {
        cachePutRequest.apply(cacheValue);
    }
    // 8. 处理 @CacheEvict
    // @CacheEvict 的 beforeInvocation 为 false 时,在目标方法执行完才执行 CacheEvict
    processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
    // 返回值
    return returnValue;
}

我们简单看一下 processCacheEvicts()、findCachedItem()、cachePutRequest.apply(),他们分别对应于对 @CacheEvict、@Cacheable、@CachePut 的处理逻辑;

1.1 processCacheEvicts()

processCacheEvicts() 用于处理 @CacheEvict 注解;

// --------------------------------- CacheAspectSupport -----------------------------------
private void processCacheEvicts(Collection<CacheOperationContext> 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);
        }
    }
}
// --------------------------------- CacheAspectSupport -----------------------------------
private void performCacheEvict(CacheOperationContext context, 
    CacheEvictOperation operation, Object result) {
    Object key = null;
    // 1. 针对每个 @CacheEvict 注解中的每个 cache 都执行处理
    for (Cache cache : context.getCaches()) {
        if (operation.isCacheWide()) {
            logInvalidating(context, operation, null);
            doClear(cache, operation.isBeforeInvocation());
        }
        else {
            if (key == null) {
                // 2. 生成 cacheKey
                key = generateKey(context, result);
            }
            logInvalidating(context, operation, key);
            // 3. 执行 doEvict()
            doEvict(cache, key, operation.isBeforeInvocation());
        }
    }
}

1.2 findCachedItem()

findCachedItem() 用于处理 @Cacheable 注解;

// --------------------------------- CacheAspectSupport -----------------------------------
private Cache.ValueWrapper findCachedItem(Collection<CacheOperationContext> contexts) {
    Object result = CacheOperationExpressionEvaluator.NO_RESULT;
    // 1. 遍历所有的 CacheOperationContext
    for (CacheOperationContext context : contexts) {
        if (isConditionPassing(context, result)) {
            Object key = generateKey(context, result);
            // 2. 如果命中了一个 @Cacheable 缓存,直接返回缓存值
            Cache.ValueWrapper cached = findInCaches(context, key);
            if (cached != null) {
                return cached;
            }
            else {
                if (logger.isTraceEnabled()) {
                    logger.trace("No cache entry for key in cache(s) " + context.getCacheNames());
                }
            }
        }
    }
    // 3. 都没命中,返回 null
    return null;
}
// --------------------------------- CacheAspectSupport -----------------------------------
private Cache.ValueWrapper findInCaches(CacheOperationContext context, Object key) {
    // 遍历 @Cacheable 中所有的 cache
    // 有一个 cache 有缓存值就返回该缓存值
    for (Cache cache : context.getCaches()) {
        Cache.ValueWrapper wrapper = doGet(cache, key);
        if (wrapper != null) {
            if (logger.isTraceEnabled()) {
                logger.trace("Cache entry for key found in cache '" + cache.getName());
            }
            return wrapper;
        }
    }
    return null;
}

1.3 cachePutRequest.apply()

cachePutRequest.apply() 用于处理 @CachePut 注解;

public void apply(@Nullable Object result) {
    if (this.context.canPutToCache(result)) {
        // 遍历 @CachePut 中所有的 cache,执行 doPut()
        for (Cache cache : this.context.getCaches()) {
            doPut(cache, this.key, result);
        }
    }
}

三. 补充

1. 方法返回值为null,会缓存吗

会缓存;

如果方法返回值为 null,此时 Cache.ValueWrapper 值如下:由于此图无法加载暂时不展示。

它是一个 SimpleValueWrapper,value 值是 null;

但是如果将注解改成如下,就不会缓存 null 值;

// result == null 的话不进行缓存
@Cacheable(cacheNames = "demoCache", key = "#id",unless = "#result == null")

2. @Cacheable注解sync=true的效果

在多线程环境下,某些操作可能使用相同参数同步调用(相同的key),默认情况下,缓存不锁定任何资源,可能导致多次计算,对数据库造成访问压力。对于这些特定的情况,属性 sync 可以指示底层将缓存锁住,使只有一个线程可以进入计算,而其他线程堵塞,直到返回结果更新到缓存中。

3. 注解可以重复标注吗

不同的注解可以标注多个,且都能生效;相同的注解不行,编译器会直接报错;如果需要相同注解标注多个等更复杂的场景,可以使用 @Caching 注解组合注解;

@CachePut(cacheNames = "demoCache", key = "#id") // 不同的注解可以标注多个
//@Cacheable(cacheNames = "demoCache", key = "#id") // 相同注解标注两个是不行的 因为它并不是@Repeatable的
@Cacheable(cacheNames = "demoCache", key = "#id")
@Override
public Object getFromDB(Integer id) {
    System.out.println("模拟去db查询~~~" + id);
    return "hello cache...";
}

编译器会直接报错,此图暂时不展示。

到此这篇关于Spring Cache原理的文章就介绍到这了,更多相关Spring Cache原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java 和 Javascript 的 Date 与 .Net 的 DateTime 之间的相互转换

    Java 和 Javascript 的 Date 与 .Net 的 DateTime 之间的相互转换

    这篇文章主要介绍了Java 和 Javascript 的 Date 与 .Net 的 DateTime 之间的相互转换的相关资料,非常不错具有参考借鉴价值,需要的朋友可以参考下
    2016-06-06
  • 理解Java垃圾回收

    理解Java垃圾回收

    这篇文章主要帮助大家理解Java垃圾回收,通过实例学习java垃圾回收,什么是垃圾回收,感兴趣的小伙伴们可以参考一下
    2016-03-03
  • SpringCloudStream中的消息分区数详解

    SpringCloudStream中的消息分区数详解

    这篇文章主要介绍了SpringCloudStream中的消息分区数,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-12-12
  • 浅谈对Java双冒号::的理解

    浅谈对Java双冒号::的理解

    这篇文章主要介绍了浅谈对Java双冒号::的理解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-06-06
  • Java 深拷贝与浅拷贝的分析

    Java 深拷贝与浅拷贝的分析

    本文主要介绍java 的深拷贝和浅拷贝,这里通过实例代码对深拷贝和浅拷贝做了详细的比较,希望能帮到有需要的小伙伴
    2016-07-07
  • Mybatis Criteria使用and和or进行联合条件查询的操作方法

    Mybatis Criteria使用and和or进行联合条件查询的操作方法

    这篇文章主要介绍了Mybatis Criteria的and和or进行联合条件查询的方法,本文通过例子给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-10-10
  • 关于IDEA关联数据库的问题

    关于IDEA关联数据库的问题

    这篇文章主要介绍了IDEA关联数据库的相关知识,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-03-03
  • java动态规划算法——硬币找零问题实例分析

    java动态规划算法——硬币找零问题实例分析

    这篇文章主要介绍了java动态规划算法——硬币找零问题,结合实例形式分析了java动态规划算法——硬币找零问题相关原理、实现方法与操作注意事项,需要的朋友可以参考下
    2020-05-05
  • MyBatis查询数据,赋值给List集合时,数据缺少的问题及解决

    MyBatis查询数据,赋值给List集合时,数据缺少的问题及解决

    这篇文章主要介绍了MyBatis查询数据,赋值给List集合时,数据缺少的问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01
  • Spring Data Jpa 复杂查询方式总结(多表关联及自定义分页)

    Spring Data Jpa 复杂查询方式总结(多表关联及自定义分页)

    这篇文章主要介绍了Spring Data Jpa 复杂查询方式总结(多表关联及自定义分页),具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-02-02

最新评论