spring-data-redis-cache 使用及源碼走讀

spring-data-redis-cache 使用及源碼走讀

版權聲明:本文爲博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處連接和本聲明。
本文連接: https://blog.csdn.net/sanri1993/article/details/102535170

預期讀者

  • 準備使用 spring 的 data-redis-cache 的同窗
  • 瞭解 @CacheConfig@Cacheable@CachePut@CacheEvict@Caching 的使用
  • 深刻理解 data-redis-cache 的實現原理

文章內容說明

  • 如何使用 redis-cache
  • 自定義 keyGenerator 和過時時間
  • 源碼解讀
  • 自帶緩存機制的不足

快速入門

  1. maven 加入 jar 包java

    <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
     
    • 1
    • 2
    • 3
    • 4
  2. 配置 redisgit

    spring.redis.host=127.0.0.1
     
    • 1
  3. 開啓 redis-cacheredis

    @EnableCaching
     
    • 1
  4. @CacheConfig@Cacheable@CachePut@CacheEvict@Caching 的功能spring

    • @Cacheable 會查詢緩存中是否有數據,若是有數據則返回,不然執行方法
    • @CachePut 每次都執行方法,並把結果進行緩存
    • @CacheEvict 會刪除緩存中的內容
    • @Caching 至關於上面三者的綜合,用於配置三者的行爲
    • @CacheConfig 配置在類上,用於配置當前類的全局緩存配置

詳細配置

通過上面的配置,就已經可使用 redis-cache 了,可是仍是有些問題須要問本身一下,好比數據庫

  • 存儲在 redis 的 key 是什麼樣子的,我能夠自定義 key 嗎
  • 存儲到 redis 的 value 是怎麼序列化的
  • 存儲的緩存是多久過時
  • 併發訪問時,會不會直接穿透從而不斷的修改緩存內容

過時時間,序列化方式由此類決定 RedisCacheConfiguration,能夠覆蓋此類達到自定義配置。默認配置爲RedisCacheConfiguration.defaultCacheConfig() ,它配置爲永不過時,key 爲 String 序列化,並加上了一個前綴作爲命名空間,value 爲 Jdk 序列化,因此你要存儲的類必需要實現 java.io.Serializable緩存

存儲的 key 值的生成由 KeyGenerator 決定,能夠在各緩存註解上進行配置,默認使用的是 SimpleKeyGenerator 其存儲的 key 方式爲 SimpleKey [參數名1,參數名2],若是在同一個命名空間下,有兩個同參數名的方法就公出現衝突致使反序列化失敗。markdown

併發訪問時,確實存在屢次訪問數據庫而沒有使用緩存的狀況 https://blog.csdn.net/clementad/article/details/52452119併發

Srping 4.3提供了一個sync參數。是當緩存失效後,爲了不多個請求打到數據庫,系統作了一個併發控制優化,同時只有一個線程會去數據庫取數據其它線程會被阻塞。app

自定義存儲 key

根據上面的說明 ,頗有可能會存在存儲的 key 一致而致使反序列化失敗,因此須要自定義存儲 key ,有兩種實現辦法 ,一種是使用元數據配置 key(簡單但難維護),一種是全局設置 keyGeneratormaven

使用元數據配置 key

@Cacheable(key = "#vin+#name") public List<Vehicle> testMetaKey(String vin,String name){ List<Vehicle> vehicles = dataProvide.selectAll(); return vehicles.stream().filter(vehicle -> vehicle.getVin().equals(vin) && vehicle.getName().contains(name)).collect(Collectors.toList()); }
 
  • 1
  • 2
  • 3
  • 4
  • 5

這是一個 spel 表達式,可使用 + 號來拼接參數,常量使用 「」 來包含,更多例子

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

注意: 生成的 key 不能爲空值,否則會報錯誤 Null key returned for cache operation

經常使用的元數據信息

名稱 位置 描述 示例
methodName root 當前被調用的方法名 #root.methodName
method root 被調用的方法對象 #root.method.name
target root 當前實例 #root.target
targetClass root 當前被調用方法參數列表 #root.targetClass
args root 當前被調用的方法名 #root.args[0]
caches root 使用的緩存列表 #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; } }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

自定義序列化和配置過時時間

由於默認使用值序列化爲 Jdk 序列化,存在體積大,增減字段會形成序列化異常等問題,能夠考慮其它序列化來覆寫默認序列化。

@Bean public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory){ RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig(); // 設置過時時間爲 30 天 redisCacheConfiguration.entryTtl(Duration.ofDays(30)); redisCacheConfiguration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new KryoRedisSerializer())); RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(redisCacheConfiguration) .withInitialCacheConfigurations(customConfigs) .build(); }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

個性化配置過時時間和序列化

上面的是全局配置過時時間和序列化,能夠針對每個 cacheNames 進行單獨設置,它是一個 Map 配置

Map<String, RedisCacheConfiguration> customConfigs = new HashMap<>(); customConfigs.put("cacheName1",RedisCacheConfiguration.defaultCacheConfig()); RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(redisCacheConfiguration) .withInitialCacheConfigurations(customConfigs) .build();
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

源碼走讀

本源碼走讀只帶你入門,具體的細節須要具體分析

首先不用看源碼也知道這確定是動態代理來實現的,代理目標方法,獲取配置,而後加強方法功能;

aop 就是幹這件事的,咱們本身也常常加一些註解來實現日誌信息採集,其實和這個原理一致,spring-data-cache-redis 也是使用 aop 實現的。

@EnableCaching 開始,能夠看到導入了一個選擇導入配置的配置類(有點繞,就是能夠本身控制導入哪些配置類),默認使用 PROXY 模式

public class CachingConfigurationSelector extends AdviceModeImportSelector<EnableCaching>
 
  • 1

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); }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

ProxyCachingConfiguration 重點的配置類是在這個配置類中,它配置了三個 Bean

BeanFactoryCacheOperationSourceAdvisorCacheOperationSource 的一個加強器

CacheOperationSource 主要提供查找方法上緩存註解的方法 findCacheOperations

CacheInterceptor 它是一個 MethodInterceptor 在調用緩存方法時,會執行它的 invoke 方法

下面來看一下 CacheInterceptorinvoke 方法

// 關鍵代碼就一句話,aopAllianceInvoker 是一個函數式接口,它會執行你的真實方法 execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());
 
  • 1
  • 2

進入 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)); } }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

再進入 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;
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

看完了執行流程,如今看一下CacheInterceptor 的超類 CacheAspectSupport ,由於我能夠不設置 cacheManager 就可使用,查看默認的 cacheManager是在哪設置的

public abstract class CacheAspectSupport extends AbstractCacheInvoker implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton { // .... }
 
  • 1
  • 2
  • 3
  • 4

BeanFactoryAware 用來獲取 BeanFactory

InitializingBean 用來管理 Bean 的生命週期,能夠在 afterPropertiesSet後添加邏輯

SmartInitializingSingleton 實現該接口後,當全部單例 bean 都初始化完成之後, 容器會回調該接口的方法 afterSingletonsInstantiated

afterSingletonsInstantiated 中,果真進行了 cacheManager 的設置,從 IOC 容器中拿了一個 cacheManger

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

那這個 CacheManager 是誰呢 ,能夠從RedisCacheConfiguration類知道答案 ,在這裏面配置了一個 RedisCacheManager

@Configuration @ConditionalOnClass(RedisConnectionFactory.class) @AutoConfigureAfter(RedisAutoConfiguration.class) @ConditionalOnBean(RedisConnectionFactory.class) @ConditionalOnMissingBean(CacheManager.class) @Conditional(CacheCondition.class) class RedisCacheConfiguration {}
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
@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()); }
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

determineConfiguration() 方法中能夠知道 cacheManager 的默認配置

最後看一下,它的切點是如何定義的,即什麼時候會調用 CacheInterceptorinvoke 方法

切點的配置是在 BeanFactoryCacheOperationSourceAdvisor 類中,返回一個這樣的切點 CacheOperationSourcePointcut ,覆寫 MethodMatcher 中的 matchs ,若是方法上存在註解 ,則認爲能夠切入。

spring-data-redis-cache 的不足

儘管功能已經很是強大,但它沒有解決緩存刷新的問題,若是緩存在某一時間過時 ,將會有大量的請求打進數據庫,會形成數據庫很大的壓力。

4.3 版本在這方面作了下併發控制,但感受比較敷衍,簡單的鎖住其它請求,先把數據 load 到緩存,而後再讓其它請求走緩存。

後面我將自定義緩存刷新,並作一個 cache 增強控件,儘可能不對原系統有太多的侵入,敬請關注

一點小推廣

創做不易,但願能夠支持下個人開源軟件,及個人小工具,歡迎來 gitee 點星,fork ,提 bug 。

Excel 通用導入導出,支持 Excel 公式
博客地址:https://blog.csdn.net/sanri1993/article/details/100601578
gitee:https://gitee.com/sanri/sanri-excel-poi

使用模板代碼 ,從數據庫生成代碼 ,及一些項目中常常能夠用到的小工具
博客地址:https://blog.csdn.net/sanri1993/article/details/98664034
gitee:https://gitee.com/sanri/sanri-tools-maven

相關文章
相關標籤/搜索