【项目demo07】自定义Redis缓存注解实现统一缓存

本文档介绍了如何在SpringBoot项目中自定义Redis缓存注解,实现统一的缓存管理。通过创建`RedisCache`注解和`RedisCacheAspect`切面类,详细说明了注解的使用、缓存的读写逻辑以及关键参数的配置,包括key生成、缓存过期时间和布隆过滤器的应用。文中还提到了使用AOP代理方法和RedisTemplate进行数据的存取操作。
摘要由CSDN通过智能技术生成

我准备开一个系列,就是写一些在简要的学习项目中可能会用到的奇奇怪怪的功能,比如线程池或者统一异常处理类
取名为【项目demo】系列
然后将会同步到GitHub中:https://github.com/Livorth/FunctionalLearning


自定义Redis缓存注解实现统一缓存

Spring Cache

这里推荐看 统一缓存帝国,实战 Spring Cache!,不能说感受良多,只能说受益匪浅

但是讲Spring Cache不是我的本意,所以Spring Cache的部分就不过多介绍,只简单描述使用过程

  1. 添加依赖:spring-boot-starter-cache
  2. 配置缓存类型,我推荐用caffeine或者redis,后面以redis为例
  3. 在主启动类或者任意配置类上加上@EnableCaching注解
  4. 最后在指定方法上添加 @Cacheable 缓存注解接口,注意他有以下参数(参考博客
    • cacheNames/value :用来指定缓存组件的名字
    • key :缓存数据时使用的 key,可以用它来指定。默认是使用方法参数的值。(这个 key 你可以使用 spEL 表达式来编写)
    • keyGenerator :key 的生成器,推荐重写。 key 和 keyGenerator 二选一使用
    • cacheManager :可以用来指定缓存管理器。从哪个缓存管理器里面获取缓存。
    • condition :可以用来指定符合条件的情况下才缓存
    • unless :否定缓存。当 unless 指定的条件为 true ,方法的返回值就不会被缓存。当然你也可以获取到结果进行判断。(通过 #result 获取方法结果)
    • sync :是否使用异步模式。

SpringBoot系列之缓存使用教程介绍了其他相关的注解


自定义Redis缓存注解

自定义注解的实现,也就是使用AOP进行切片处理

这里我主要参考

上面这个相对简单,下面这个相当复杂,不过我还是一下面这个为蓝本来写的

自定义缓存注解RedisCache

因为是自定义的,所以可以根据自己的需求来进行变动,我按照我自己的想法进行了一定的改动

package cn.livorth.functionallearning.common.cache;

import java.lang.annotation.*;

/**
 * @program: FunctionalLearning
 * @description: 缓存注解
 * 此注解还可以使用布隆过滤器,对数据库和缓存中都不存在的查询放进过滤器,防止缓存击穿攻击;
 * @author: livorth
 * @create: 2021-10-05 21:50
 **/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
@Documented
public @interface RedisCache {

    // key的前缀
    String nameSpace() default "";

    // key
    String key() default "";

    // 设置过期时间,默认1分钟
    long expireTime() default 1 * 60 * 1000;

    // 是否为查询操作,如果为写入数据库的操作,该值需置为 false
    boolean read() default true;

}
具体AOP代理方法类RedisCacheAspect

和常规的AOP代理方法类相同,主要的是其中实现的具体逻辑

  1. 根据相关信息生成key
  2. 通过key在redis中查询数据是非已经存在
    • 如果不存在,则去数据库中查找,如果数据库中都不存在,则要考虑内存穿透的问题了
    • 如果存在,则返回redis中对应的数据,同时记得需要反序列化,毕竟存储的时候已经统一JSON化了

其中有两个地方需要注意

  1. key的生成方案,仅仅考传入的"key"作为key肯定是不够的,必然会重复,由于我暂时对springEL表达式不是很了解,所以我是用的是MD5加密同时进行拼接的方式来生成的
  2. 反序列的过程,比如泛型的判断
package cn.livorth.functionallearning.common.cache;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
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.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.concurrent.TimeUnit;

/**
 * @program: FunctionalLearning
 * @description: Redis缓存切面操作
 * @author: livorth
 * @create: 2021-10-05 21:56
 **/
@Aspect
@Component
@Slf4j
public class RedisCacheAspect {

    @Resource
    private RedisHandler handler;

    @Pointcut(value = "@annotation(cn.livorth.functionallearning.common.cache.RedisCache)")
    public void redisCache() {
    }

    // 在使用 redisCache 注解的地方织入此切点
    @Around(value = "redisCache()")
    private Object saveCache(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {

        log.info("<======拦截到redisCache方法:{}.{}======>" ,
                proceedingJoinPoint.getTarget().getClass().getName(), proceedingJoinPoint.getSignature().getName());

        // 获取切入的方法对象
        // 这个m是代理对象的,没有包含注解
        Method m = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();
        // this()返回代理对象,target()返回目标对象,目标对象反射获取的method对象才包含注解
        Method methodWithAnnotations = proceedingJoinPoint.getTarget().getClass().getDeclaredMethod(
                proceedingJoinPoint.getSignature().getName(), m.getParameterTypes());

        Object result;
        // 根据目标方法对象获取注解对象
        RedisCache annotation = methodWithAnnotations.getDeclaredAnnotation(RedisCache.class);

        // 解析key
        String key = parseKey(methodWithAnnotations, proceedingJoinPoint.getArgs(), annotation.key(), annotation.nameSpace());

        // 到redis中获取缓存
        log.info("<====== 通过key:{}从redis中查询 ======>", key);
        String cache = handler.getCache(key);
        if (cache == null) {
            log.info("<====== Redis 中不存在该记录,从数据库查找 ======>");
            // 若不存在,则到数据库中去获取
            result = proceedingJoinPoint.proceed();
            if (result != null) {
                // 从数据库获取后存入redis, 若有指定过期时间,则设置
                long expireTime = annotation.expireTime();
                if (expireTime != -1) {
                    handler.saveCache(key, result, expireTime, TimeUnit.SECONDS);
                } else {
                    handler.saveCache(key, result);
                }
            }
//            else {
//                // 这里可以做一个布隆过滤器的处理
//            }
            return result;
        } else {
            // 如果缓存中存在数据
            return deSerialize(m, cache);
        }
    }

    /**
     * 反序列化
     * @param m 原方法的对应信息
     * @param cache
     * @return
     */
    private Object deSerialize(Method m, String cache) {
        // 原方法的返回数据类型类
        Class returnTypeClass = m.getReturnType();
        log.info("从缓存中获取数据:{},返回类型为:{}" , cache, returnTypeClass);
        Object object = null;
        // 原方法的返回数据类型类
        Type returnType = m.getGenericReturnType();
        // 判断是否是ParameterizedType的实例,即泛型
        if(returnType instanceof ParameterizedType){
            ParameterizedType type = (ParameterizedType) returnType;
            Type[] typeArguments = type.getActualTypeArguments();
            for(Type typeArgument : typeArguments){
                // 如果是泛型则需要将其中每个单独转换
                Class typeArgClass = (Class) typeArgument;
                log.info("<======获取到泛型:{}" , typeArgClass.getName());
                object = JSON.parseArray(cache, typeArgClass);
            }
        }else {
            // 不是泛型则直接转换
            object = JSON.parseObject(cache, returnTypeClass);
        }
        return object;
    }


    /**
     * 解析springEL表达式,生成key
     * @param method 原方法
     * @param argValues 输入参数
     * @param key key
     * @param nameSpace 命名空间
     * @return
     */
    private String parseKey(Method method, Object[] argValues, String key, String nameSpace) {
        // 创建解析器,但是Spring表达式语言我并不是很熟悉,所以这里暂时不考虑使用
//        ExpressionParser parser = new SpelExpressionParser();
//        Expression expression = parser.parseExpression(key);
//        EvaluationContext context = new StandardEvaluationContext();
//
//        // 添加参数
//        DefaultParameterNameDiscoverer discover = new DefaultParameterNameDiscoverer();
//        String[] parameterNames = discover.getParameterNames(method);
//        for (int i = 0; i < parameterNames.length; i++) {
//            context.setVariable(parameterNames[i], argValues[i]);
//        }
//        // 解析
//        return /*method.getName() + ":" +*/ nameSpace + expression.getValue(context).toString();

//        简单点也可以 命名空间+key+方法名+参数的MD5加密
        StringBuilder prefix = new StringBuilder();
        prefix.append(nameSpace).append(".").append(key);
        prefix.append(".").append(method.getName());
        StringBuilder sb = new StringBuilder();
        for (Object obj : argValues) {
            sb.append(obj.toString());
        }
        return prefix.append(DigestUtils.md5DigestAsHex(sb.toString().getBytes())).toString();
    }


    @Component
    class RedisHandler {

        @Resource
        RedisTemplate<String, String> cache;

        <T> void saveCache(String key, T t, long expireTime, TimeUnit unit) {
            String value = JSON.toJSONString(t);
            log.info("<====== 存入Redis 数据:{}", value);
            cache.opsForValue().set(key, value, expireTime, unit);
        }

        <T> void saveCache(String key, T t) {
            String value = JSON.toJSONString(t, SerializerFeature.WRITE_MAP_NULL_FEATURES);
            cache.opsForValue().set(key, value);
        }

        void removeCache(String key) {
            cache.delete(key);
        }

        String getCache(String key) {
            return cache.opsForValue().get(key);
        }

    }

}

注意这里面的RedisHandler其实可以使用之前自己写的redis封装类RedisUtils

然后就是布隆过滤器的使用,其实我只知道概念,具体使用暂时还不清楚

进行测试

说是测试也只是加个注解的事情

UserController.java

/**
* /user/page/3/5
* @param thePage 当前多少页
* @param pageSize 每页多少个
* @return
*/
@LogAnnotation(logModule = "user", logType = "select",  logDescription = "通过分页获取所有用户信息")
@RedisCache(nameSpace = "user", key = "getAllUserByPage")
@GetMapping("page/{thePage}/{pageSize}")
public List<User> getAllUserByPage(@PathVariable("thePage") int thePage, @PathVariable("pageSize") int pageSize){
    Page<User> page = new Page<>(thePage, pageSize);
    return userService.getAllUserByPage(page);
}

然后进行多次访问并查看日志信息

http://localhost:8888/user/page/3/5
在这里插入图片描述在这里插入图片描述
第一次是对数据库中信息进行访问,第二次测试从缓存中找


写在后面

注解自定义,代理方法类自己实现,很多东西的决定权在自己手上

要想简单写可以写的很简单,要写复杂那都不是几百行可以搞定的

只能说看需求来吧,我这也只能起到一个抛砖引玉的作用


我的项目demo系列都会同步到GitHub上,欢迎围观

https://github.com/Livorth/FunctionalLearning

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值