緩存概述 解決不同設備間速度不匹配問題。 互聯網分層架構:降低資料庫壓力,提升系統整體性能,縮短訪問時間。 高併發問題 緩存併發(擊穿):緩存過期後將嘗試從後端資料庫獲取數據 緩存穿透:不存在的 key,請求直接落庫查詢 緩存雪崩:緩存大面積失效,請求直接落庫查詢 需求說明 通過在方法上增加緩存註解 ...
緩存概述
解決不同設備間速度不匹配問題。
互聯網分層架構:降低資料庫壓力,提升系統整體性能,縮短訪問時間。
高併發問題
- 緩存併發(擊穿):緩存過期後將嘗試從後端資料庫獲取數據
- 緩存穿透:不存在的 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 = 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
推薦閱讀:
作者:EastX本文來自博客園,轉載請註明原文鏈接:https://www.cnblogs.com/cnx01/p/16818865.html