spring-data-redis-cache 使用及原始碼走讀
預期讀者
- 準備使用 spring 的 data-redis-cache 的同學
- 瞭解
@CacheConfig
,@Cacheable
,@CachePut
,@CacheEvict
,@Caching
的使用 - 深入理解 data-redis-cache 的實現原理
文章內容說明
- 如何使用 redis-cache
- 自定義 keyGenerator 和過期時間
- 原始碼解讀
- 自帶快取機制的不足
快速入門
maven 加入 jar 包
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
配置 redis
spring.redis.host=127.0.0.1
開啟 redis-cache
@EnableCaching
@CacheConfig
,@Cacheable
,@CachePut
,@CacheEvict
,@Caching
的功能@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],如果在同一個名稱空間下,有兩個同參數名的方法就公出現衝突導致反序列化失敗。
併發訪問時,確實存在多次訪問資料庫而沒有使用快取的情況 https://blog.csdn.net/clementad/article/details/52452119
Srping 4.3提供了一個sync引數。是當快取失效後,為了避免多個請求打到資料庫,系統做了一個併發控制優化,同時只有一個執行緒會去資料庫取資料其它執行緒會被阻塞。
自定義儲存 key
根據上面的說明 ,很有可能會存在儲存的 key 一致而導致反序列化失敗,所以需要自定義儲存 key ,有兩種實現辦法 ,一種是使用元資料配置 key(簡單但難維護),一種是全域性設定 keyGenerator
使用元資料配置 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());
}
這是一個 spel 表示式,可以使用 + 號來拼接引數,常量使用 "" 來包含,更多例子
@Cacheable(value = "user",key = "targetClass.name + '.'+ methodName")
@Cacheable(value = "user",key = "'list'+ targetClass.name + '.'+ methodName + #name ")
注意: 生成的 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;
}
}
自定義序列化和配置過期時間
因為預設使用值序列化為 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();
}
個性化配置過期時間和序列化
上面的是全域性配置過期時間和序列化,可以針對每一個 cacheNames 進行單獨設定,它是一個 Map 配置
Map<String, RedisCacheConfiguration> customConfigs = new HashMap<>();
customConfigs.put("cacheName1",RedisCacheConfiguration.defaultCacheConfig());
RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
.withInitialCacheConfigurations(customConfigs)
.build();
原始碼走讀
本原始碼走讀只帶你入門,具體的細節需要具體分析
首先不用看原始碼也知道這肯定是動態代理來實現的,代理目標方法,獲取配置,然後增強方法功能;
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 的不足
儘管功能已經非常強大,但它沒有解決快取重新整理的問題,如果快取在某一時間過期 ,將會有大量的請求打進資料庫,會造成資料庫很大的壓力。
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-ma