通过注解实现接口自动缓存Redis和数据过期功能

通过注解实现接口自动缓存Redis和数据过期功能

一. 概述

1.1 Redis是什么?

  • 在java中,常见的数据持久化有几种类型。
    • 关系型数据库:MySQL 、Orcale 、 SQL Server等。
    • 非关系型数据库: MongDB、Redis、Solr、ElasticSearch等。
  • 总结:Redis是一个非关系型数据库。它的业务执行是在内存中进行的,因此性能高,同时它是单线程的,一定程度上是线程安全的。如果接口查询速度很慢,我们可以提前存入Redis,当接口接收到同样的请求参数时,即可从Redis中直接拿取数据,不必再进行逻辑处理。如果数据要求及时性,我们可以通过设置Redis的过期时间来保证数据的及时更新。

1.1 为什么要通过注解实现接口自动缓存Redis和数据过期功能?

  • 部分接口查询速度慢,且通过优化也无法有效提升查询速度时,需要通过查询Redis进行提速;
  • 使用注解,操作简单方便、代码侵入小、工作量低、易于维护、设计优雅。

使用Redis做缓存就是,在首次查询时,将查询结果存入了Redis数据库中,后续的查询均可以直接从Redis取值,而不必做逻辑处理,这样做能很快的返回数据,提高用户体验。

1.2 使用缓存功能的业务展示:

  • 使用注解:
    在这里插入图片描述
  • 调用接口:
    在这里插入图片描述
  • 我们调用接口后,检查Redis里面是否存在这个接口所缓存的数据:
    在这里插入图片描述

说明我们调用此接口缓存成功了。调用这个接口的时候,会自动获取key和参数,并做缓存。下次调这个接口的时候,如果遇到相同的参数则直接返回结果。

  • 再使用其他参数测试是否能分别缓存,发现能分别存入。功能实现成功!
    • 输入其他参数调用接口:
      在这里插入图片描述
    • 发现能动态存入Redis,如图:
      在这里插入图片描述

    注意,在图中,我们设置的这个接口的expire过期时间为1分钟,所以过期时间很快,需要注意。在调用接口的过程中,发现接口返回的速度明显提高,说明已先走Redis进行查询了。也可以用Thred.sleep(3000L) 线程睡眠来模拟查询慢的接口。

二. 手把手实战实现Redis注解缓存

2.1 实现步骤

  • 博主使用环境:java8 + SpringBoot+SpringCloud+SpringDataRedis
  • 使用目录如图:
    • 包结构:
      在这里插入图片描述

      RedisUtils这个只是作为存入Redis的工具。在看懂源码的基础上,小伙伴们可以将RedisUtils替换为自己公司或者自己喜欢的Redis存入方式,也可以直接使用。

  • 如果你在你的工程中已经配置好了Redis的配置时,即可直接使用。使用方式就是将这四个类直接拷贝到你所在的项目中,然后在你需要缓存的地方加上@RedisCache注解即可。(注解内需要配置Key和field,expire看需要,如果不配置则会自动使用注解内的默认exipre的值)

@Pointcut("@annotation(com.xxx.common.redis.annotation.RedisCache)") 这里的RedisCache指向的路径是RedisCache的路径。你需要进行修改,修改为自己工程中RedisCache所在的位置路径。其他同理。

2.2 注解类 1:

package com.cdmtc.annotation.redis;

import java.lang.annotation.*;


/**
 * @program: cdmtc.commond.platform
 * @description: redis缓存对象
 * @author: 暗余
 * @create: 2019-12-05
 */
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisCache {
    /**
     * 键名
     *
     * @return
     */
    String key() default "";

    /**
     * 主键
     *
     * @return
     * @author zmr
     */
    String fieldKey();

    /**
     * 过期时间
     *
     * @return
     */
    long expired() default 0;         //  默认一天

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

2.3 注解类2:

package com.cdmtc.annotation.redis;

import java.lang.annotation.*;

/**
 * @program: cdmtc.commond.platform
 * @description: redis缓存对象
 * @author: 暗余
 * @create: 2019-12-05
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisEvict {
    String key();

    String fieldKey();
}

2.4 RedisUtils类

package com.cdmtc.annotation.redis;


import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * Redis工具类
 */
@Component
public class RedisUtils {


    @Autowired
    private  RedisTemplate redisTemplate;

    @Resource(name = "stringRedisTemplate")
    private ValueOperations<String, String> valueOperations;

    /**
     * 默认过期时长,单位:秒
     */
    public final static long DEFAULT_EXPIRE = 60 * 60 * 24;

    /**
     * 不设置过期时长
     */
    public final static long NOT_EXPIRE = -1;

    /**
     * 插入缓存默认时间
     *
     * @param key   键
     * @param value 值
     * @author zmr
     */
    public void set(String key, Object value) {
        set(key, value, DEFAULT_EXPIRE);
    }

    /**
     * 插入缓存
     *
     * @param key    键
     * @param value  值
     * @param expire 过期时间(s)
     * @author zmr
     */
    public void set(String key, Object value, long expire) {
        valueOperations.set(key, toJson(value));
        redisTemplate.expire(key, expire, TimeUnit.SECONDS);

    }

    /**
     * 返回字符串结果
     *
     * @param key 键
     * @return
     * @author zmr
     */
    public String get(String key) {
        return valueOperations.get(key);
    }

    /**
     * 返回指定类型结果
     *
     * @param key   键
     * @param clazz 类型class
     * @return
     * @author zmr
     */
    public <T> T get(String key, Class<T> clazz) {
        String value = valueOperations.get(key);
        return value == null ? null : fromJson(value, clazz);
    }

    /**
     * 删除缓存
     *
     * @param key 键
     * @author zmr
     */
    public void delete(String key) {
        redisTemplate.delete(key);
    }

    public void delectUserInfo(String key, String token) {
        BoundHashOperations boundHashOperations = redisTemplate.boundHashOps(key);
        boundHashOperations.delete(token);
    }

    ;

    /**
     * Object转成JSON数据
     */
    private String toJson(Object object) {
        if (object instanceof Integer || object instanceof Long || object instanceof Float || object instanceof Double
                || object instanceof Boolean || object instanceof String) {
            return String.valueOf(object);
        }
        return JSON.toJSONString(object);
    }

    /**
     * JSON数据,转成Object
     */
    private <T> T fromJson(String json, Class<T> clazz) {
        return JSON.parseObject(json, clazz);
    }

    /**
     * 获取过期时间
     */
    public Long getTime(String key) {
        return redisTemplate.getExpire(key);
    }

    /**
     * hash入对象
     */
    public void setForObject(String key, Map<String, Object> map, Long time) {
        BoundHashOperations boundHashOperations = redisTemplate.boundHashOps(key);
        Set<String> keySet = map.keySet();
        for (String s : keySet) {
            boundHashOperations.put(s, map.get(s));
            boundHashOperations.expire(time, TimeUnit.MICROSECONDS);
        }
    }

    public Object getObject(String Bigkey, String SmallKey) {
        BoundHashOperations boundHashOperations = redisTemplate.boundHashOps(Bigkey);
        return boundHashOperations.get(SmallKey);
    }

    /**
     * 插入Set
     */
    public void setForSet(String key, String value, Long time) {
        BoundSetOperations<String, Object> boundSetOperations = redisTemplate.boundSetOps(key);
        boundSetOperations.add(value);
        boundSetOperations.expire(time, TimeUnit.SECONDS);
    }

    /**
     * 是否存在于SET
     *
     * @param key
     */
    public Boolean isInSet(String key, String value) {
        BoundSetOperations<String, Object> boundSetOperations = redisTemplate.boundSetOps(key);
        return boundSetOperations.isMember(value);
    }

    /**
     * 加锁
     *
     * @param key   商品id
     * @param value 当前时间+超时时间
     * @return
     */
    public boolean lock(String key, String value) {
        if (redisTemplate.opsForValue().setIfAbsent(key, value)) {
            redisTemplate.expire(key, 1000, TimeUnit.SECONDS);
            return true;
        }

        //避免死锁,且只让一个线程拿到锁
        String currentValue = redisTemplate.opsForValue().get(key).toString();
        //如果锁过期了
        if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) {
            //获取上一个锁的时间
            String oldValues = redisTemplate.opsForValue().getAndSet(key, value).toString();

            /*
               只会让一个线程拿到锁
               如果旧的value和currentValue相等,只会有一个线程达成条件,因为第二个线程拿到的oldValue已经和currentValue不一样了
             */
            if (!StringUtils.isEmpty(oldValues) && oldValues.equals(currentValue)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 解锁
     *
     * @param key
     * @param value
     */
    public void unlock(String key, String value) {
        try {
            String currentValue = redisTemplate.opsForValue().get(key).toString();
            if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
                redisTemplate.opsForValue().getOperations().delete(key);
                System.out.println("释放锁");
            }
        } catch (Exception e) {
            System.out.println("解锁异常");
        }
    }
}

2.5 切面类

package com.ruoyi.common.redis.aspect;

import java.lang.reflect.Method;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
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 com.ruoyi.common.redis.annotation.RedisCache;
import com.ruoyi.common.redis.annotation.RedisEvict;
import com.ruoyi.common.redis.util.RedisUtils;

@Component
@Aspect
public class RedisAspect {
    private final static Logger logger = LoggerFactory.getLogger(RedisAspect.class);

    @Autowired
    private RedisUtils redis;

    /**
     * 定义切入点,使用了 @RedisCache 的方法
     */
    @Pointcut("@annotation(com.xxx.common.redis.annotation.RedisCache)")
    public void redisCachePoint() {
    }

    @Pointcut("@annotation(com.xxx.common.redis.annotation.RedisEvict)")
    public void redisEvictPoint() {
    }

    @After("redisEvictPoint()")
    public void evict(JoinPoint point) {
        Method method = ((MethodSignature) point.getSignature()).getMethod();
        RedisEvict redisEvict = method.getAnnotation(RedisEvict.class);
        // 获取RedisCache注解
        String fieldKey = parseKey(redisEvict.fieldKey(), method, point.getArgs());
        String rk = redisEvict.key() + ":" + fieldKey;
        logger.debug("<======切面清除rediskey:{} ======>" + rk);
        redis.delete(rk);
    }

    /**
     * 环绕通知,方法拦截器
     */
    @Around("redisCachePoint()")
    public Object WriteReadFromRedis(ProceedingJoinPoint point) {
        try {
            Method method = ((MethodSignature) point.getSignature()).getMethod();
            // 获取RedisCache注解
            RedisCache redisCache = method.getAnnotation(RedisCache.class);
            Class<?> returnType = ((MethodSignature) point.getSignature()).getReturnType();
            if (redisCache != null && redisCache.read()) {
                // 查询操作
                logger.debug("<======method:{} 进入 redisCache 切面 ======>", method.getName());
                String fieldKey = parseKey(redisCache.fieldKey(), method, point.getArgs());
                String rk = redisCache.key() + ":" + fieldKey;
                Object obj = redis.get(rk, returnType);
                if (obj == null) {
                    // Redis 中不存在,则从数据库中查找,并保存到 Redis
                    logger.debug("<====== Redis 中不存在该记录,从数据库查找 ======>");
                    obj = point.proceed();
                    if (obj != null) {
                        if (redisCache.expired() > 0) {
                            redis.set(rk, obj, redisCache.expired());
                        } else {
                            redis.set(rk, obj);
                        }
                    }
                }
                return obj;
            }
        } catch (Throwable ex) {
            logger.error("<====== RedisCache 执行异常: {} ======>", ex);
        }
        return null;
    }

    /**
     * 获取缓存的key
     * key 定义在注解上,支持SPEL表达式
     *
     * @param pjp
     * @return
     */
    private String parseKey(String key, Method method, Object[] args) {
        // 获取被拦截方法参数名列表(使用Spring支持类库)
        LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
        String[] paraNameArr = u.getParameterNames(method);
        // 使用SPEL进行key的解析
        ExpressionParser parser = new SpelExpressionParser();
        // SPEL上下文
        StandardEvaluationContext context = new StandardEvaluationContext();
        // 把方法参数放入SPEL上下文中
        for (int i = 0; i < paraNameArr.length; i++) {
            context.setVariable(paraNameArr[i], args[i]);
        }
        return parser.parseExpression(key).getValue(context, String.class);
    }
}

三. 分析源码

3.1 概述:

  • 通过源码可以看到,注解缓存的实现逻辑很清晰;就是通过
    ① 注解获取类名参数名方法名
    ② 切面RedisAspect类中的环绕通知方法WriteReadFromRedis进行处理;
    ③ 通过反射获取到注解截获的参数,然后从Redis查询出对应的value值。
    ④ 先判断查询出来的值是否为空
    ⑤ 如果为空则通过类名方法名以及参数进行反射调用原方法拿到最新的数据,然后存入Redis,再返回给前端。
    ⑥ 如果不为空,则直接返回前端

    实际就是一个拦截器。始终是没有进入Controller内部代码的。通过切面直接进行处理了。

3.2 注解类:

  • 概述:第一个参数就是 redis存入的key 第二个就是参数名(与key拼接组成新key) expired设置过期时间
    在这里插入图片描述
    在这里插入图片描述

3.3 切面类分析:

在这里插入图片描述

代码实现逻辑如上。在能理解代码逻辑的基础上,是可以继续加功能或者继续完善的。

发布了127 篇原创文章 · 获赞 52 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_37128049/article/details/103425867