Redis

关注公众号 jb51net

关闭
首页 > 数据库 > Redis > Redis 声明式注解缓存查询

Redis+自定义注解+AOP实现声明式注解缓存查询的示例

作者:​​​​​​​ 铲子Zzz

实际项目中,会遇到很多查询数据的场景,这些数据更新频率也不是很高,一般我们在业务处理时,会对这些数据进行缓存,本文主要介绍了Redis+自定义注解+AOP实现声明式注解缓存查询的示例,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧

引言:为什么需要声明式缓存?

核心流程设计

方法调用 → 切面拦截 → 生成缓存Key → 查询Redis → 
└ 命中 → 直接返回缓存数据
└ 未命中 → 加锁查DB → 结果写入Redis → 返回数据

二、核心实现步骤

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 卫相yang
 * OverSion03
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisCache {
    /**
     * Redis键前缀(支持SpEL表达式)
     */
    String key();

    /**
     * 过期时间(默认1天)
     */
    long expire() default 1;

    /**
     * 时间单位(默认天)
     */
    TimeUnit timeUnit() default TimeUnit.DAYS;

    /**
     * 是否缓存空值(防穿透)
     */
    boolean cacheNull() default true;
}

2. 编写AOP切面(核心逻辑)

切面职责

空值缓存:缓存NULL值并设置短过期时间,防止恶意攻击

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 卫相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表达式生成完整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);
        }
        // 执行原方法
        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 + 动态参数
            expire = 1, //过期时间
            timeUnit = TimeUnit.DAYS // 时间单位
    )
    @Override
    public ReturnVO<List<GoodsCategory>> listHomeSecondGoodsCategory(Integer level, Integer categoryType) {
        // 数据库查询
        List<GoodsCategory> dbList = goodsCategoryMapper.selectList(
                new LambdaQueryWrapper<GoodsCategory>()
                        .eq(GoodsCategory::getCategoryLevel, level)
                        .eq(GoodsCategory::getCategoryType, categoryType)
                        .eq(GoodsCategory::getIsHomePage, 1)
                        .orderByDesc(GoodsCategory::getHomeSort)
        );
        // 设置父级UUID(可优化为批量查询)
        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);
    }

最终效果:

到此这篇关于Redis+自定义注解+AOP实现声明式注解缓存查询的示例的文章就介绍到这了,更多相关Redis 声明式注解缓存查询内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文