IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 多级缓存分析篇(三) Spring本地缓存源码分析 -> 正文阅读

[Java知识库]多级缓存分析篇(三) Spring本地缓存源码分析

上篇讲了Redis的分布式锁,这篇讲下Spring本地缓存策略,对源码稍作分析,以便设计多级缓存了解需要注意的细节坑,比如本地缓存和redis缓存的失效时间冲突等。

1 spring-data系列说明

日常开发中,一般都会需要框架对各种主流数据源的支持,spring官方就是利用了仓储动态代理机制,基于AbstractFactory(抽象工厂)、Adapter(适配器)、Template(模板)、Strategy(策略)、Visitor(访问者)多种设计模式,提供了spring-data-*这样一套完整的数据持久化的支持能力。

框架分析主要包括:

1)spring-context:spring上下文管理核心包,基本上只要用到spring框架都会有。这里提供了CacheOperation来存放本地缓存的元数据信息,比如本地cacheKey,代理后的cacheManager和cacheResolver类名等。

2)spring-data-common: spring-data系列核心包,完成了spring容器持久化仓储对象的能力,通过定义适配数据源的Operation概念(例如MongoDbOperation、RedisCacheOperation等)来构建上下文。

3)spring-data-*适配包:例如spring-data-redis、spring-data-mongodb、spring-data-jpa等,其中本节主要是基于redis和本地缓存的桥接(对应的是spring-data-redis-cache)来分析源码的实现。

2 redis-cache包说明

2.1 使用方式

spring对redis和本地缓存是通过spring-data-redis-cache来桥接的。

官方提供了这些注解,方便我们平常使用:

@CacheConfig、@Cacheable、@CachePut,@CacheEvict、@Caching,这里列举下他们的功能:

@Cacheable 会查询缓存中是否有数据,如果有数据则返回,否则执行方法
@CachePut 每次都执行方法,并把结果进行缓存
@CacheEvict 会删除缓存中的内容
@Caching 相当于上面三者的综合,用于配置三者的行为
@CacheConfig 配置在类上,用于配置当前类的全局缓存配置

2.2 配置详解

经过上面的配置,就已经可以使用 redis-cache 的能力了,但是还是有些问题需要了解清楚比较好,比如:

存储在 redis 的 key 是什么样子的,可以自定义 key么?
存储到 redis 的 value 是如何序列化的?
存储的缓存是多久过期?
并发访问时,会不会直接穿透从而不断的修改缓存内容?

这里对这些疑问,简单列举下解决方案:

1)过期时间、序列化方式都是由?RedisCacheConfiguration类决定的。可以覆盖此类达到自定义配置。默认配置为RedisCacheConfiguration.defaultCacheConfig() ,它配置为永不过期,key 为 String 序列化,并加上了一个前缀做为命名空间,value 为 Jdk 序列化,所以你要存储的类必须要实现 java.io.Serializable 。

2)存储的 key 值的生成由 KeyGenerator 决定,可以在各缓存注解上进行配置,默认使用的是 SimpleKeyGenerator 其存储的 key 方式为 SimpleKey [参数名1,参数名2],如果在同一个命名空间下,有两个同参数名的方法就公出现冲突导致反序列化失败。

3)并发访问时,确实存在多次访问数据库而没有使用缓存的情况 (可以参考https://blog.csdn.net/clementad/article/details/52452119比较详细)

Srping 4.3提供了一个sync参数。是当缓存失效后,为了避免多个请求打到数据库,系统做了一个并发控制优化,同时只有一个线程会去数据库取数据其它线程会被阻塞。

4)自定义存储 key
根据上面的说明 ,很有可能会存在存储的 key 一致而导致反序列化失败,所以需要自定义存储 key ,有两种实现办法 ,一种是使用元数据配置 key(简单但难维护),一种是全局设置 keyGenerator。

使用元数据配置 key:

    @Cacheable(key = "#ord+#name")
    public List<Order> testMetaKey(String orderNo,String name){
        List<Order> orders = dataProvide.selectAll();
        return orders.stream().filter(od -> od.getOrderNo().equals(orderNo) && od.getName().contains(name)).collect(Collectors.toList());
    }

也可以是一个SPEL 表达式,即使用 + 号来拼接参数,常量使用""来包含,例如

@Cacheable(value = "user",key = "targetClass.name   '.'  methodName")
@Cacheable(value = "user",key = "'list' + targetClass.name + '.'  methodName +  #name ")

注意:?需自行保证 生成的 key 不能为空值,不然会报错误 Null key returned for cache operation。

常用的元数据信息包括:

名称位置描述示例
methodNameroot当前被调用的方法名#root.methodName
methodroot被调用的方法对象#root.method.name
targetroot当前实例#root.target
targetClassroot当前被调用方法参数列表#root.targetClass
argsroot当前被调用的方法名#root.args[0]
cachesroot使用的缓存列表#root.caches[0].name
Argument Name执行上下文方法参数数据#user.id
result执行上下文方法返回值数据#result.id

使用全局 keyGenerator

使用元数据的特点是简单,但是难维护,如果需要配置的缓存接口较多的话,这时可以配置一个 keyGenerator ,这个配置配置多个,引用其名称即可。

@Bean
public KeyGenerator cacheKeyGenerator() {
    return (target, method, params) -> {
        return target+method+params;
    }
}

5)自定义序列化和配置过期时间

因为默认使用值序列化为 Jdk 序列化,存在体积大,增减字段会造成序列化异常等问题,可以考虑其它序列化来覆写默认序列化。示例:

@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory){
    RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
    // 设置过期时间为10 天
    redisCacheConfiguration.entryTtl(Duration.ofDays(10));
    redisCacheConfiguration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new KryoRedisSerializer()));
    RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory)
				.cacheDefaults(redisCacheConfiguration)
				.withInitialCacheConfigurations(customConfigs)
				.build();
}

上面的是全局配置过期时间和序列化,其实也可以针对每一个 cacheNames 进行单独设置,它是一个 Map 配置:

Map<String, RedisCacheConfiguration> customConfigs = new HashMap<>();
customConfigs.put("cacheName1",RedisCacheConfiguration.defaultCacheConfig());

RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory)
				.cacheDefaults(redisCacheConfiguration)
				.withInitialCacheConfigurations(customConfigs)
				.build();

3?spring-data-redis-cache源码分析

首先不用看源码也知道这肯定是动态代理来实现的,代理目标方法,获取配置,然后增强方法功能;

AOP 就是干这件事的,我们自己也经常加一些注解来实现日志信息采集,其实和这个原理一致,spring-data-cache-redis 也是使用 aop 实现的。

从?@EnableCaching?开始,可以看到导入了一个选择导入配置的配置类(即可以自己控制导入哪些配置类),默认使用?PROXY?模式:
?

public class CachingConfigurationSelector extends AdviceModeImportSelector<EnableCaching>

//PROXY模式导入了如下配置类

private String[] getProxyImports() {
    List<String> result = new ArrayList<>(3);
    result.add(AutoProxyRegistrar.class.getName());
    result.add(ProxyCachingConfiguration.class.getName());
    if (jsr107Present && jcacheImplPresent) {
        result.add(PROXY_JCACHE_CONFIGURATION_CLASS);
    }
    return StringUtils.toStringArray(result);
}

ProxyCachingConfiguration?重点的配置类是在这个配置类中,它配置了三个 Bean

BeanFactoryCacheOperationSourceAdvisor?是?CacheOperationSource?的一个增强器

CacheOperationSource?主要提供查找方法上缓存注解的方法?findCacheOperations

CacheInterceptor?它是一个?MethodInterceptor?在调用缓存方法时,会执行它的?invoke?方法

下面来看一下?CacheInterceptor?的?invoke?方法

// 关键代码就一句话,aopAllianceInvoker 是一个函数式接口,它会执行你的真实方法
execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());

进入?execute?方法,可以看到这一层只是获取到所有的缓存操作集合,@CacheConfig@Cacheable@CachePut@CacheEvict@Caching?然后把其配置和当前执行上下文进行绑定成了?CacheOperationContexts

Class<?> targetClass = getTargetClass(target);
CacheOperationSource 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));
    }
}

再进入?execute?方法,可以看到前面专门是对?sync?做了处理,后面才是对各个注解的处理

if (contexts.isSynchronized()) {
    // 这里是专门于 sync 做的处理,可以先不去管它,后面再来看是如何处理的,先看后面的内容 
}

// 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 如果缓存没有命中,收集 put 请求,后面会统一把需要放入缓存中的统一应用
List<CachePutRequest> cachePutRequests = new LinkedList<>();
if (cacheHit == null) {
    collectPutRequests(contexts.get(CacheableOperation.class),
                       CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
}

Object cacheValue;
Object returnValue;

// 缓存有命中并且不是 @CachePut 的处理
if (cacheHit != null && !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 把前面收集到的所有 putRequest 数据放入缓存
for (CachePutRequest cachePutRequest : cachePutRequests) {
    cachePutRequest.apply(cacheValue);
}

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

return returnValue;

看完了执行流程,现在看一下CacheInterceptor?的超类?CacheAspectSupport?,因为可以不设置?cacheManager?就可以使用,查看默认的?cacheManager是在哪设置的

public abstract class CacheAspectSupport extends AbstractCacheInvoker
		implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {
	// .... 
}

BeanFactoryAware 用来获取 BeanFactory

InitializingBean 用来管理 Bean 的生命周期,可以在?afterPropertiesSet后添加逻辑

SmartInitializingSingleton 实现该接口后,当所有单例 bean 都初始化完成以后, 容器会回调该接口的方法?afterSingletonsInstantiated

在?afterSingletonsInstantiated?中,果然进行了?cacheManager?的设置,从 IOC 容器中拿了一个 cacheManger

setCacheManager(this.beanFactory.getBean(CacheManager.class));

那这个?CacheManager?是谁呢 ,可以从RedisCacheConfiguration类知道答案 ,在这里面配置了一个?RedisCacheManager

@Configuration
@ConditionalOnClass(RedisConnectionFactory.class)
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory   redisConnectionFactory,
                                      ResourceLoader resourceLoader) {
        RedisCacheManagerBuilder builder = RedisCacheManager
        .builder(redisConnectionFactory)
        .cacheDefaults(determineConfiguration(resourceLoader.getClassLoader()));

        List<String> cacheNames = this.cacheProperties.getCacheNames();
        if (!cacheNames.isEmpty()) {
            builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
        }
        return this.customizerInvoker.customize(builder.build());
    }

}

从?determineConfiguration()?方法中可以知道 cacheManager 的默认配置

最后看一下,它的切点是如何定义的,即何时会调用?CacheInterceptor?的?invoke?方法

切点的配置是在?BeanFactoryCacheOperationSourceAdvisor?类中,返回一个这样的切点?CacheOperationSourcePointcut?,覆写?MethodMatcher?中的?matchs?,如果方法上存在注解 ,则认为可以切入。

spring-data-redis-cache 的不足

尽管功能已经非常强大,但它没有解决缓存刷新的问题,如果缓存在某一时间过期 ,将会有大量的请求打进数据库,会造成数据库很大的压力。

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-01-24 10:41:48  更:2022-01-24 10:43:53 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 9:37:56-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码