通过注解的方式,实现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 缓存:
.