Redis+自定義注解+AOP實現(xiàn)聲明式注解緩存查詢的示例
更新時間:2025年04月02日 10:18:20 作者:????????鏟子Zzz
實際項目中,會遇到很多查詢數(shù)據(jù)的場景,這些數(shù)據(jù)更新頻率也不是很高,一般我們在業(yè)務(wù)處理時,會對這些數(shù)據(jù)進(jìn)行緩存,本文主要介紹了Redis+自定義注解+AOP實現(xiàn)聲明式注解緩存查詢的示例,文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
引言:為什么需要聲明式緩存?
- 背景痛點(diǎn):傳統(tǒng)代碼中緩存邏輯與業(yè)務(wù)邏輯高度耦合,存在重復(fù)代碼、維護(hù)困難等問題(如手動判斷緩存存在性、序列化/反序列化操作)
- 解決方案:通過注解+AOP實現(xiàn)緩存邏輯與業(yè)務(wù)解耦,開發(fā)者只需關(guān)注業(yè)務(wù),通過注解配置緩存策略(如過期時間、防擊穿機(jī)制等)
- 技術(shù)價值:提升代碼可讀性、降低維護(hù)成本、支持動態(tài)緩存策略擴(kuò)展。
核心流程設(shè)計:
方法調(diào)用 → 切面攔截 → 生成緩存Key → 查詢Redis → └ 命中 → 直接返回緩存數(shù)據(jù) └ 未命中 → 加鎖查DB → 結(jié)果寫入Redis → 返回數(shù)據(jù)
二、核心實現(xiàn)步驟
1. 定義自定義緩存注解(如@RedisCache)
package com.mixchains.ytboot.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
/**
* @author 衛(wèi)相yang
* OverSion03
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisCache {
/**
* Redis鍵前綴(支持SpEL表達(dá)式)
*/
String key();
/**
* 過期時間(默認(rèn)1天)
*/
long expire() default 1;
/**
* 時間單位(默認(rèn)天)
*/
TimeUnit timeUnit() default TimeUnit.DAYS;
/**
* 是否緩存空值(防穿透)
*/
boolean cacheNull() default true;
}
2. 編寫AOP切面(核心邏輯)
切面職責(zé):
- 緩存Key生成:拼接類名、方法名、參數(shù)哈希(MD5或SpEL動態(tài)參數(shù))本次使用的是SpEL
- 緩存查詢:優(yōu)先從Redis讀取,使用FastJson等工具反序列化
空值緩存:緩存NULL值并設(shè)置短過期時間,防止惡意攻擊
package com.mixchains.ytboot.common.aspect;
import com.alibaba.fastjson.JSON;
import com.mixchains.ytboot.common.annotation.RedisCache;
import io.micrometer.core.instrument.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
/**
* @author 衛(wèi)相yang
* OverSion03
*/
@Aspect
@Component
@Slf4j
public class RedisCacheAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private final ExpressionParser parser = new SpelExpressionParser();
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
@Around("@annotation(redisCache)")
public Object around(ProceedingJoinPoint joinPoint, RedisCache redisCache) throws Throwable {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
// 解析SpEL表達(dá)式生成完整key
String key = parseKey(redisCache.key(), method, joinPoint.getArgs());
// 嘗試從緩存獲取
String cachedValue = redisTemplate.opsForValue().get(key);
if (StringUtils.isNotBlank(cachedValue)) {
Type returnType = ((MethodSignature) joinPoint.getSignature()).getReturnType();
return JSON.parseObject(cachedValue, returnType);
}
// 執(zhí)行原方法
Object result = joinPoint.proceed();
// 處理緩存存儲
if (result != null || redisCache.cacheNull()) {
String valueToCache = result != null ?
JSON.toJSONString(result) :
(redisCache.cacheNull() ? "[]" : null);
if (valueToCache != null) {
redisTemplate.opsForValue().set(
key,
valueToCache,
redisCache.expire(),
redisCache.timeUnit()
);
}
}
return result;
}
private String parseKey(String keyTemplate, Method method, Object[] args) {
String[] paramNames = parameterNameDiscoverer.getParameterNames(method);
EvaluationContext context = new StandardEvaluationContext();
if (paramNames != null) {
for (int i = 0; i < paramNames.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
}
return parser.parseExpression(keyTemplate).getValue(context, String.class);
}
}
代碼片段示例:
@RedisCache(
key = "'category:homeSecond:' + #categoryType", //緩存的Key + 動態(tài)參數(shù)
expire = 1, //過期時間
timeUnit = TimeUnit.DAYS // 時間單位
)
@Override
public ReturnVO<List<GoodsCategory>> listHomeSecondGoodsCategory(Integer level, Integer categoryType) {
// 數(shù)據(jù)庫查詢
List<GoodsCategory> dbList = goodsCategoryMapper.selectList(
new LambdaQueryWrapper<GoodsCategory>()
.eq(GoodsCategory::getCategoryLevel, level)
.eq(GoodsCategory::getCategoryType, categoryType)
.eq(GoodsCategory::getIsHomePage, 1)
.orderByDesc(GoodsCategory::getHomeSort)
);
// 設(shè)置父級UUID(可優(yōu)化為批量查詢)
List<Long> parentIds = dbList.stream().map(GoodsCategory::getParentId).distinct().collect(Collectors.toList());
Map<Long, String> parentMap = goodsCategoryMapper.selectBatchIds(parentIds)
.stream()
.collect(Collectors.toMap(GoodsCategory::getId, GoodsCategory::getUuid));
dbList.forEach(item -> item.setParentUuid(parentMap.get(item.getParentId())));
return ReturnVO.ok("列出首頁二級分類", dbList);
}最終效果:

到此這篇關(guān)于Redis+自定義注解+AOP實現(xiàn)聲明式注解緩存查詢的示例的文章就介紹到這了,更多相關(guān)Redis 聲明式注解緩存查詢內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
記錄一次并發(fā)情況下的redis導(dǎo)致服務(wù)假死的問題解決
由于Redis需要依賴于操作系統(tǒng)環(huán)境,如果系統(tǒng)資源受限,比如過量的進(jìn)程在擠占系統(tǒng)資源、系統(tǒng)死鎖等情況,本文主要介紹了記錄一次并發(fā)情況下的redis導(dǎo)致服務(wù)假死的問題解決,感興趣的可以了解一下2023-09-09
redis.clients.jedis.exceptions.JedisDataException異常的錯誤解決
本文主要介紹了redis.clients.jedis.exceptions.JedisDataException異常的錯誤解決,這個異常通常發(fā)生在嘗試連接到一個?Redis?服務(wù)器時,客戶端發(fā)送了一個?AUTH?命令來驗證密碼,但是沒有配置密碼驗證,下來就來解決一下2024-05-05
淺析對redis?hashtable?的sizemask理解
在?Redis?的哈希表實現(xiàn)中,index?=?hash?&?dict->ht[0].sizemask?是計算鍵值對應(yīng)存儲位置的核心操作,本文給大家介紹redis?hashtable?的sizemask理解,感興趣的朋友一起看看吧2025-03-03

