快取概述
解決不同設備間速度不匹配問題,
互聯網分層架構:降低資料庫壓力,提升系統整體性能,縮短訪問時間,
高并發問題
- 快取并發(擊穿):快取過期后將嘗試從后端資料庫獲取資料
- 快取穿透:不存在的 key,請求直接落庫查詢
- 快取雪崩:快取大面積失效,請求直接落庫查詢
需求說明
- 通過在方法上增加快取注解,呼叫方法時根據指定 key 快取回傳資料,再次呼叫從快取中獲取
- 可通過注解指定不同的快取時長
- 避免快取擊穿:快取失效后使用互斥鎖限制查庫數量
- 避免快取穿透:對于 null 支持短時間存盤
- 避免快取雪崩:可支持每個 key 增加隨機時長
一、Spring Cache 整合 Redis 實作
利用 Spring Cache 處理 Redis 快取資料
Spring Cache 注解@Cacheable攜帶的sync()屬性可支持互斥鎖限制單個執行緒處理,可避免快取擊穿
注意開啟 Spring Cache 需要在配置類(或啟動類)上增加@EnableCaching
1 :快取管理器注入配置時,處理 快取空間 cacheNames() / value() 與時長對應
可根據 配置或代碼寫死 指定不同 快取空間 快取時長
此方式以 快取空間名 為標識區分時長,未配置的快取空間走全域設定
點擊查看代碼 ExpandRedisConfig.java
# yml 配置
expand-cache-config:
ttl-map: '{"yml-ttl":1000,"hello":2000}'
// 引入配置
@Value("#{${expand-cache-config.ttl-map:null}}")
private Map<String, Long> ttlMap;
/**
* 注入快取管理器及處理配置中的快取時長
*/
@Bean(BEAN_REDIS_CACHE_MANAGER)
public RedisCacheManager expandRedisCacheManager(RedisConnectionFactory factory) {
/*
使用 Jackson 作為值序列化處理器
FastJson 存在部分轉換問題如:Set 存盤后因為沒有對應的型別保存無法轉換為 JSONArray(實作 List ) 導致失敗
*/
ObjectMapper om = JsonUtil.createJacksonObjectMapper();
GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer(om);
// 配置key、value 序列化(解決亂碼的問題)
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
// key 使用 string 序列化方式
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(StringRedisSerializer.UTF_8))
// value 使用 jackson 序列化方式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer))
// 配置快取空間名稱前綴
.prefixCacheNameWith("spring:cache:")
// 配置全域快取過期時間
.entryTtl(Duration.ofMinutes(30L));
// 專門指定某些快取空間的配置,如果過期時間,這里的 key 為快取空間名稱
Map<String, RedisCacheConfiguration> configMap = new HashMap<>();
// 代碼寫死示例
configMap.put("world", config.entryTtl(Duration.ofSeconds(60)));
Set<Map.Entry<String, Long>> entrySet =
Optional.ofNullable(ttlMap).map(Map::entrySet).orElse(Collections.emptySet());
for (Map.Entry<String, Long> entry : entrySet) {
// 指定特定快取空間對應的過期時間
configMap.put(entry.getKey(), config.entryTtl(Duration.ofSeconds(entry.getValue())));
}
RedisCacheWriter redisCacheWriter = RedisCacheWriter.lockingRedisCacheWriter(factory);
// 使用自定義快取管理器附帶自定義引數隨機時間,注意此處為全域設定,5-最小隨機秒,30-最大隨機秒
return new ExpandRedisCacheManager(redisCacheWriter, config, configMap, 5, 30);
}
2 :在快取空間名 cacheNames() / value() 中附帶時間字串
自定義快取管理器繼承
RedisCacheManager,重寫創建快取處理器方法,拿到快取空間名與快取配置進行更新快取時長處理
此方式以 快取空間名中非指定時間部分 為標識區分時長,快取空間名不指定時間走全域設定
使用示例:@Cacheable(cacheNames = "prefix#5m", cacheManager = "expandRedisCacheManager")
點擊查看代碼 ExpandRedisCacheManager.java
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
String theName = name;
if (name.contains(NAME_SPLIT_SYMBOL)) {
// 名稱中存在#標記,修改實際名稱,替換默認配置的快取時長為指定快取時長
String[] nameArr = name.split(NAME_SPLIT_SYMBOL);
theName = nameArr[0];
Duration duration = TimeUtil.parseDuration(nameArr[1]);
if (duration != null) {
cacheConfig = cacheConfig.entryTtl(duration);
}
}
// 使用自定義快取處理器附帶自定義引數隨機時間,將注入的隨機時間傳遞
return new ExpandRedisCache(theName, cacheWriter, cacheConfig, this.minRandomSecond,
this.maxRandomSecond);
}
3 :自定義快取注解支持 Spring Cache
- 自定義注解 使用
@Cacheable標識,可支持 Spring Cache 處理 - 自定義注解增加設定快取時長的屬性:
timeout() + unit(),需要與注入注解的初始化配置方生效 - 自定義快取注解過期時間初始化配置,利用 Spring Component Bean 獲取到使用自定義注解的方法,利用反射獲取注解屬性并設定快取空間過期時間;Map 處理,同一名稱快取空間將會出現替換情景
- 此處理實質仍是以快取空間名
cacheNames() / value()中非時間部分為標識區分時長
點擊查看代碼 ExpandCacheExpireConfig.java
/**
* Spring Bean 加載后處理
* 獲取所有 @Component 注解的 Bean 判斷類中方法是否存在 @SpringCacheable 注解,存在進行過期時間設定
*/
@PostConstruct
public void init() {
Map<String, Object> beanMap = beanFactory.getBeansWithAnnotation(Component.class);
if (MapUtil.isEmpty(beanMap)) {
return;
}
beanMap.values().forEach(item ->
ReflectionUtils.doWithMethods(item.getClass(), method -> {
ReflectionUtils.makeAccessible(method);
putConfigTtl(method);
})
);
expandRedisCacheManager.initializeCaches();
}
/**
* 利用反射設定方法注解上配置的過期時間
* @param method 注解了自定義快取的方法
*/
private void putConfigTtl(Method method) {
ExpandCacheable annotation = method.getAnnotation(ExpandCacheable.class);
if (annotation == null) {
return;
}
String[] cacheNames = annotation.cacheNames();
if (ArrayUtil.isEmpty(cacheNames)) {
cacheNames = annotation.value();
}
// 反射獲取快取管理器初始化配置并設值
Map<String, RedisCacheConfiguration> initialCacheConfiguration =
(Map<String, RedisCacheConfiguration>)
ReflectUtil.getFieldValue(expandRedisCacheManager, "initialCacheConfiguration");
RedisCacheConfiguration defaultCacheConfig =
(RedisCacheConfiguration)
ReflectUtil.getFieldValue(expandRedisCacheManager, "defaultCacheConfig");
Duration ttl = Duration.ofSeconds(annotation.unit().toSeconds(annotation.timeout()));
for (String cacheName : cacheNames) {
initialCacheConfiguration.put(cacheName, defaultCacheConfig.entryTtl(ttl));
}
}
4 :自定義快取處理器,在設定快取時處理時長
繼承 Spring 快取處理器
RedisCache,重寫設定快取方法
可針對 null 進行短時間存盤避免快取穿透、增加隨機時長避免快取雪崩
點擊查看代碼 ExpandRedisCache.java
@Override
public void put(Object key, @Nullable Object value) {
Object cacheValue = https://www.cnblogs.com/cnx01/p/preProcessCacheValue(value);
// 替換父類設定快取時長處理
Duration duration = getDynamicDuration(cacheValue);
cacheWriter.put(name, createAndConvertCacheKey(key),
serializeCacheValue(cacheValue), duration);
}
/**
* 獲取動態時長
*/
private Duration getDynamicDuration(Object cacheValue) {
// 如果快取值為 null,固定回傳時長為 30s 避免快取穿透
if (NullValue.INSTANCE.equals(cacheValue)) {
return Duration.ofSeconds(30);
}
int randomInt = RandomUtil.randomInt(this.minRandomSecond, this.maxRandomSecond);
return this.cacheConfig.getTtl().plus(Duration.ofSeconds(randomInt));
}
小結
- 優點:使用 Spring 自帶功能,通用性強
- 缺點:針對快取空間處理快取時長,快取時間一致可能導致快取雪崩,自定義處理需要理解相應原始碼實作
參考:
- Spring cache整合Redis,并給它一個過期時間!
- 讓 @Cacheable 可配置 Redis 過期時間
- @Cacheable注解配合Redis設定快取隨機失效時間
- 聊聊如何基于spring @Cacheable擴展實作快取自動過期時間以及自動重繪
- SpringBoot實作Redis快取(SpringCache+Redis的整合)
二、自定義 AOP 實作
使用 Spring AOP 切面對注解攔截以支持方法呼叫結果進行快取,
1. 注解屬性定義
- 支持不同型別快取 key:
key() + keyType() - 支持依據條件( SpEL 運算式)設定排除不走快取:
unless() - 支持快取 key 自定義過期時長( Redis 快取):
timeout() + unit() - 支持快取 key 自定義過期時長增加隨機時長( Redis 快取):
addRandomDuration(),注意固定了隨機范圍,可避免快取雪崩 - 支持本地快取設定:
useLocal() + localTimeout(),注意本地快取存在全域最大時長限制
2. AOP 切面處理
- 快取存盤資料時加鎖(synchronized)執行,避免快取擊穿
- 對 null 值進行固定格式字串快取,避免快取穿透
點擊查看代碼 MethodCacheAspect.java
/**
* 利用 AOP 環繞通知對注解方法回傳進行快取處理
*/
@Around("@annotation(cn.eastx.practice.demo.cache.config.custom.MethodCacheable)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodCacheableOperation operation = MethodCacheableOperation.convert(joinPoint);
if (Objects.isNull(operation)) {
return joinPoint.proceed();
}
Object result = getCacheData(operation);
if (Objects.nonNull(result)) {
return convertCacheData(result);
}
// 加鎖處理同步執行
synchronized (operation.getKey().intern()) {
result = getCacheData(operation);
if (Objects.nonNull(result)) {
return convertCacheData(result);
}
result = joinPoint.proceed();
setDataCache(operation, result);
}
return result;
}
/**
* 設定資料快取
* 特殊值快取需要轉換,特殊值包括 null
* @param operation 操作物件
* @param data 資料
*/
private void setDataCache(MethodCacheableOperation operation, Object data) {
// null快取處理,固定存盤時長,防止快取穿透
if (Objects.isNull(data)) {
redisUtil.setEx(operation.getKey(), NULL_VALUE, SPECIAL_VALUE_DURATION);
return;
}
// 存在實際資料快取處理
redisUtil.setEx(operation.getKey(), data, operation.getDuration());
if (Boolean.TRUE.equals(operation.getUseLocal())) {
LocalCacheUtil.put(operation.getKey(), data, operation.getLocalDuration());
}
}
小結
- 優點:自定義 Spring AOP 實作,可定制化處理程度較高,當前以支持兩級快取(Redis 快取 + 本地快取)
- 缺點:相對于 Spring 自帶 Cache ,部分功能存在缺失不夠完善
其他
本文 demo 地址:https://github.com/EastX/java-practice-demos/tree/main/demo-cache
推薦閱讀:
- 快取那些事 - 美團技術團隊 明輝
- 高并發之快取 - 開拖拉機的蠟筆小新
本文來自博客園,轉載請注明原文鏈接:https://www.cnblogs.com/cnx01/p/16818865.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/519014.html
標籤:Java
上一篇:【Netty 從成神到升仙系列 大結局】全網一圖流死磕決議 Netty 原始碼
下一篇:日志
