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

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

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

  • 背景痛点:传统代码中缓存逻辑与业务逻辑高度耦合,存在重复代码、维护困难等问题(如手动判断缓存存在性、序列化/反序列化操作) 
  • 解决方案:通过注解+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切面(核心逻辑)

切面职责

  • 缓存Key生成:拼接类名、方法名、参数哈希(MD5或SpEL动态参数)本次使用的是SpEL
  • 缓存查询:优先从Redis读取,使用FastJson等工具反序列化  

空值缓存:缓存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 声明式注解缓存查询内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Redis的配置、启动、操作和关闭方法

    Redis的配置、启动、操作和关闭方法

    今天小编就为大家分享一篇Redis的配置、启动、操作和关闭方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2018-05-05
  • 一文带你了解Redis的三种集群模式

    一文带你了解Redis的三种集群模式

    Redis 的常用的集群方式主要有以下三种,分别是主从复制模式、哨兵模式、Redis-Cluster集群模式,那么下面我们就分别了解一下这三种集群模式的优点与缺点
    2023-06-06
  • 详解redis中的锁以及使用场景

    详解redis中的锁以及使用场景

    这篇文章主要介绍了详解redis中的锁以及使用场景,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • redis哨兵模式说明与搭建详解

    redis哨兵模式说明与搭建详解

    这篇文章主要介绍了redis哨兵模式说明与搭建详解,需要的朋友可以参考下
    2023-01-01
  • redis 实现登陆次数限制的思路详解

    redis 实现登陆次数限制的思路详解

    这篇文章主要介绍了redis 实现登陆次数限制的思路详解,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-08-08
  • 关于Redis bigkeys命令会阻塞问题的解决

    关于Redis bigkeys命令会阻塞问题的解决

    这篇文章主要介绍了关于Redis bigkeys命令会阻塞问题的解决,今天分享一次Redis引发的线上事故,避免再次踩雷,实现快速入门,需要的朋友可以参考下
    2023-03-03
  • redis发布和订阅_动力节点Java学院整理

    redis发布和订阅_动力节点Java学院整理

    这篇文章主要为大家详细介绍了redis发布和订阅的相关资料,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-08-08
  • 一文教你学会Redis的事务

    一文教你学会Redis的事务

    Redis 作为内存的存储中间件,已经是面试的面试题必问之一了。今天小编就来和大家一起来聊聊Redis的事务吧,希望对大家有所帮助
    2022-08-08
  • Redis消息队列实现异步秒杀功能

    Redis消息队列实现异步秒杀功能

    在高并发场景下,为了提高秒杀业务的性能,可将部分工作交给 Redis 处理,并通过异步方式执行,Redis 提供了多种数据结构来实现消息队列,总结三种,本文详细介绍Redis消息队列实现异步秒杀功能,感兴趣的朋友一起看看吧
    2025-04-04
  • Redis缓存过期的实现示例

    Redis缓存过期的实现示例

    Redis缓存的过期策略是保证缓存可靠性和性能的关键之一,本文主要介绍了Redis缓存过期的实现示例,具有一定的参考价值,感兴趣的可以了解一下
    2023-12-12

最新评论