通过注解的方式,实现Redis 自动查找缓存,以及未命中时自动更新缓存

1、写在前面的话

 
       常在项目中使用 Redis 缓存以提高查询效率。遇到查询时一般的套路是,先去查Redis缓存,如果查询未命中缓存就去查数据库,并将数据库查询的数据放在缓存,下次查询时就可以直接查询缓存。
 
       写这篇博客的目的就是直接通过注解的方式,来完成上面的步骤。在编码时在业务层查询方法上,只要添加了注解,就自动处理了优先查缓存,以及未命中缓存时,对数据库的查询结果放入缓存。那么我们在业务层的查询方法,只需要关注未命中缓存时查询数据库的操作,提高编码效率。
 

2、你们可以白嫖的代码

2.1、需要引入 jar 包

 
在pom文件中引入以下jar,需要说明的是我用的是 SpringBoot 构建的项目

        <!-- 添加 Redis依赖 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>
		<!---->

2.2、yml 配置文件

 
这是使用 Redis 最基本的配置

spring:
  redis:
    host: 127.0.0.1
  #Redis服务器连接端口
    port: 6379
  #Redis服务器连接密码(默认为空)
    password:
  #连接超时时间(毫秒)
    timeout: 30000

2.3、Redis 的配置类



import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * redis配置类
 * @author STRANGE-P
 * @date 
 */
@Configuration
public class RedisConfig {
    
    

    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
    
    
        RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}


2.4、定义注解


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 定义注解
 * @author STRANGE-P
 * @date 
 */
@Target({
    
    ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisCacheable {
    
    

    /** 第一过期时间 **/
    long firstLayerTtl() default 10L;

    /** 第一过期时间 **/
    long secondLayerTtl() default 60L;

    /** Redis key 值 **/
    String key();

}

2.5、@Aspect 处理切面


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.CodeSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.Expression;
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.util.concurrent.TimeUnit;


/**
 * 
 * @author STRANGE-P
 * @date 
 */
@Aspect
@Component
public class RedisCacheableAspect {
    
    

    private static final Logger log = LoggerFactory.getLogger(RedisCacheableAspect.class);
    private static final ExpressionParser expressionParser = new SpelExpressionParser();
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public RedisCacheableAspect(RedisTemplate<String, Object> redisTemplate) {
    
    
        this.redisTemplate = redisTemplate;
    }

    @Pointcut("@annotation(redisCacheable)")
    public void RedisCacheablePointcut(RedisCacheable redisCacheable) {
    
    
    }

    private StandardEvaluationContext getContextContainingArguments(ProceedingJoinPoint joinPoint) {
    
    
        StandardEvaluationContext context = new StandardEvaluationContext();
        // 通过Java反射,解析ProceedingJoinPoint的方法参数及参数值
        CodeSignature codeSignature = (CodeSignature)joinPoint.getSignature();
        // 获取入参对象中的所有参数名
        String[] parameterNames = codeSignature.getParameterNames();
        // 获取连接点(joinPoint)的方法运行时的入参列表
        Object[] args = joinPoint.getArgs();

        for(int i = 0; i < parameterNames.length; ++i) {
    
    
            context.setVariable(parameterNames[i], null == args[i] ? "null" : args[i]);
        }

        return context;
    }

    private String getCacheKeyFromAnnotationKeyValue(StandardEvaluationContext context, String key) {
    
    
        // 表达式解析器,解析注解中的 key 值
        Expression expression = expressionParser.parseExpression(key);
        return (String)expression.getValue(context);
    }

    @Around("RedisCacheablePointcut(redisCacheable)")
    public Object cacheTwoLayered(ProceedingJoinPoint joinPoint, RedisCacheable redisCacheable) throws Throwable {
    
    
        // 获取注解中的第一过期时间
        long firstLayerTtl = redisCacheable.firstLayerTtl();
        // 获取注解中的第二过期时间
        long secondLayerTtl = redisCacheable.secondLayerTtl();
        // 获取注解中的 key 值
        String key = redisCacheable.key();
        StandardEvaluationContext context = this.getContextContainingArguments(joinPoint);
        String cacheKey = this.getCacheKeyFromAnnotationKeyValue(context, key);
        log.info("### Cache key: {}", cacheKey);
        // 获取系统当前时间
        long start = System.currentTimeMillis();
        Object result;
        // 如果缓存中存在 当前 key 的数据
        if (this.redisTemplate.hasKey(cacheKey)) {
    
    
            // 通过 key 获取 redis 缓存值
            result = this.redisTemplate.opsForValue().get(cacheKey);
            log.info("Reading from cache ..." + result.toString());
            // 当缓存中的剩余过期时间,小于第二过期时间时,不取缓存中的数据,查询数据库
            if (this.redisTemplate.getExpire(cacheKey, TimeUnit.MINUTES) < secondLayerTtl) {
    
    
                try {
    
    
                    result = joinPoint.proceed();
                    // 将查询结果放入 Redis 缓存,并设置过期时间,过期时间为 第一过期时间+第二过期时间
                    this.redisTemplate.opsForValue().set(cacheKey, result, secondLayerTtl + firstLayerTtl, TimeUnit.MINUTES);
                } catch (Exception var15) {
    
    
                    log.warn("An error occured while trying to refresh the value - extending the existing one", var15);
                    this.redisTemplate.opsForValue().getOperations().expire(cacheKey, secondLayerTtl + firstLayerTtl, TimeUnit.MINUTES);
                }
            }
        } else {
    
    
            result = joinPoint.proceed();
            log.info("Cache miss: Called original method");
            // 将查询结果放入 Redis 缓存,并设置过期时间,过期时间为 第一过期时间+第二过期时间
            this.redisTemplate.opsForValue().set(cacheKey, result, firstLayerTtl + secondLayerTtl, TimeUnit.MINUTES);
        }
        // 获取执行时间
        long executionTime = System.currentTimeMillis() - start;
        log.info("{} executed in {} ms", joinPoint.getSignature(), executionTime);
        log.info("Result: {}", result);
        return result;
    }

}

2.6 业务层注解使用示例


import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class RedisTestServiceImpl {
    
    

	    /**
	     *  这里的 firstLayerTtl + secondLayerTtl = TTL 过期时间
	     *  当缓存中的剩余过期时间,小于 secondLayerTtl 时间时,不取缓存中的数据,查询数据库
	     *  firstLayerTtl 和 secondLayerTtl 时间单位均是 分钟
	     *  详细逻辑,可以看看 RedisCacheableAspect 类
	     **/
	    @RedisCacheable(key = "'gof:com:test'",firstLayerTtl = 20L,secondLayerTtl = 10L)
	    public User methodTest1(){
    
    
	
	           log.info("-- 查询开始");
	           // 这里处理未命中缓存时,查询数据库的逻辑
	           // todo……
	           log.info("-- 查询结束 --");
	           return user;
	    }
	
	    /**
	     * 这里可以使用通配符,key 作为 Redis 的 key 值,
	     * key会拼接成为 gof:com:test:入参name的值+入参salt的值
	     **/
	    @RedisCacheable(key = "'gof:com:test:'.concat(#name).concat(#salt)",firstLayerTtl = 20L,secondLayerTtl = 10L)
	    public User methodTest2(String name, String salt){
    
    
	        log.info("-- 查询开始");
	        // 这里处理未命中缓存时,查询数据库的逻辑
	        // todo……
	        log.info("-- 查询结束 --");
	        return user;
	    }
    

}

3、让你们看看效果

 
测试代码我随便写的,没用 @Data 相关注解和数据库查询,不是博主菜,你懂得……
 

3.1、测试的实体类

package com.ttt.gof.entity;

import java.util.Date;

public class User {
    
    
    private Integer userId;

    private String userName;

    private String userPhone;

    private String password;

    private String salt;

    private Date createTime;

    private Integer createUser;

    private Date modifyTime;

    private Integer modifyUser;

    public Integer getUserId() {
    
    
        return userId;
    }

    public void setUserId(Integer userId) {
    
    
        this.userId = userId;
    }

    public String getUserName() {
    
    
        return userName;
    }

    public void setUserName(String userName) {
    
    
        this.userName = userName == null ? null : userName.trim();
    }

    public String getUserPhone() {
    
    
        return userPhone;
    }

    public void setUserPhone(String userPhone) {
    
    
        this.userPhone = userPhone == null ? null : userPhone.trim();
    }

    public String getPassword() {
    
    
        return password;
    }

    public void setPassword(String password) {
    
    
        this.password = password == null ? null : password.trim();
    }

    public String getSalt() {
    
    
        return salt;
    }

    public void setSalt(String salt) {
    
    
        this.salt = salt == null ? null : salt.trim();
    }

    public Date getCreateTime() {
    
    
        return createTime;
    }

    public void setCreateTime(Date createTime) {
    
    
        this.createTime = createTime;
    }

    public Integer getCreateUser() {
    
    
        return createUser;
    }

    public void setCreateUser(Integer createUser) {
    
    
        this.createUser = createUser;
    }

    public Date getModifyTime() {
    
    
        return modifyTime;
    }

    public void setModifyTime(Date modifyTime) {
    
    
        this.modifyTime = modifyTime;
    }

    public Integer getModifyUser() {
    
    
        return modifyUser;
    }

    public void setModifyUser(Integer modifyUser) {
    
    
        this.modifyUser = modifyUser;
    }
}

3.2、业务层测试代码


import com.ttt.gof.entity.User;
import com.qiuwan.gof.rediscache.twolayer.aop.RedisCacheable;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class RedisTestServiceImpl {
    
    


    @RedisCacheable(key = "'gof:com:test'", firstLayerTtl = 20L, secondLayerTtl = 10L)
    public User methodTest1() {
    
    

        log.info("-- 查询开始");
        User user = new User();
        user.setSalt("param_salt");
        user.setPassword("param_password");
        user.setUserName("param_userName");
        log.info("-- 查询结束 --");
        return user;
    }

    @RedisCacheable(key = "'gof:com:test:'.concat(#name).concat(#salt)", firstLayerTtl = 20L, secondLayerTtl = 10L)
    public User methodTest2(String name, String salt) {
    
    

        log.info("-- 查询开始");
        User user = new User();
        user.setSalt("param_salt");
        user.setPassword("param_password");
        user.setUserName("param_userName");
        log.info("-- 查询结束 --");
        return user;
    }

}

3.3、控制层测试代码

package com.ttt.gof.controller;

import com.ttt.gof.entity.User;
import com.ttt.gof.service.impl.RedisTestServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class RedisTestController {
    
    


    @Autowired
    private RedisTestServiceImpl redisTestService;

    @RequestMapping("/testApi1")
    @ResponseBody
    public User testApi() {
    
    
        return redisTestService.methodTest1();
    }


    @RequestMapping("/testApi2")
    @ResponseBody
    public User testApi2() {
    
    
        return redisTestService.methodTest2("张三", "碘盐");
    }
}

3.4、调用接口

  • 调用前先让你们看看 Redis 缓存 空空如也
     
    在这里插入图片描述
     

3.4.1、testApi1 接口

  • 第一次调用 testApi1 接口

 
在这里插入图片描述
 
控制台:
 
在这里插入图片描述
 
Redis 缓存:
 
在这里插入图片描述
 

  • 第二次调用 testApi1 接口
     
    在这里插入图片描述
     
    控制台:
     
    在这里插入图片描述
     
    Redis 缓存:
     
    在这里插入图片描述

3.4.1、testApi2 接口

  • 第一次调用 testApi2 接口
     
    在这里插入图片描述
     
    控制台:
     
    在这里插入图片描述
     
    Redis 缓存:
     
    在这里插入图片描述
     
  • 第二次调用 testApi2 接口
     
    在这里插入图片描述
     
    控制台:
     
    在这里插入图片描述 
    Redis 缓存:
     
    在这里插入图片描述
     
     
     
     
     
     
     
     
     
     
     
     
    .

猜你喜欢

转载自blog.csdn.net/weixin_41922349/article/details/109843213