在某个mysql+redis的业务中,有个根据id查询用户表数据逻辑,逻辑为:
1、先从redis中查询,如果redis中有,则返回redis结果
2、如果redis中没有,则查询mysql
3、将查出的结果放入redis中,并在方法中返回该查询结果
代码如下:
@Override
public User getUserById(Integer id) {
User user = null;
//1 拼装缓存key
String key = CACHE_KEY_USER + id;
//2 查询redis
user = (User)redisTemplate.opsForValue().get(key);
//3 redis无,进一步查询mysql
if (user == null) {
//3.1 从mysql查出来user
user = this.userMapper.selectByPrimaryKey(id);
//3.2 mysql有,redis无
if (user != null) {
//3.3 把mysql捞到的数据写入redis,方便下次查询能redis命中。
redisTemplate.opsForValue().set(key, user);
}
}
//4 redis有,直接返回
return user;
}
这样写逻辑是没有问题的,但是如果以后有更多类似的需求,难道每个方法里面都要写这么长一串的重复判断逻辑吗?并且今天业务可能根据id来查询,明天需求变更,根据username查询,要改的话就比较麻烦了
因此,我们可以通过热拔插AOP+反射+Redis自定义缓存注解+SpringEL表达式,重构缓存代码,思路来源链接
通过编写通用组件,代码可精简到如下程度,以后有类似的mysql+redis需求,也只需要一个注解即可使用。通过修改#后面的值,保持值和方法传参名一致
@Override
@MyRedisCache(keyPrefix = "user",matchValue = "#id")
public User getUserById(Integer id){
return userMapper.selectByPrimaryKey(id);
}
项目结构:标准的Springboot
pom.xml:引入mybatisplus、mysql驱动、lombok、redis、asepct相关
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.example</groupId>
<artifactId>my-redis</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>my-redis</name>
<description>my-redis</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.10.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.20</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.22.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties:配置了redis和sql数据源
spring.application.name=my-redis
server.port=8081
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/你的库?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
spring.datasource.username=你的账号
spring.datasource.password=你的密码
spring.data.redis.database=0
spring.data.redis.host=127.0.0.1
spring.data.redis.port=6379
spring.data.redis.password=
spring.data.redis.lettuce.pool.max-active=8
spring.data.redis.lettuce.pool.max-wait=-1ms
spring.data.redis.lettuce.pool.max-idle=8
spring.data.redis.lettuce.pool.min-idle=0
启动类:无改动,保持默认
package org.example.myredis;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MyRedisApplication {
public static void main(String[] args) {
SpringApplication.run(MyRedisApplication.class, args);
}
}
RedisConfig:标准的序列化配置,注意额外加上注解@EnableAspectJAutoProxy,作用是开启AOP自动代理
package org.example.myredis.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
*
* @author xiajunfeng
*/
@Configuration
@EnableAspectJAutoProxy //V2 开启AOP自动代理
public class RedisConfig {
/**
* @param lettuceConnectionFactory
* @return redis序列化的工具配置类,下面这个请一定开启配置
* 127.0.0.1:6379> keys *
* 1) "ord:102" 序列化过
* 2) "\xac\xed\x00\x05t\x00\aord:102" 野生,没有序列化过
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
//设置key序列化方式string
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置value的序列化方式json
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
实体类User:记得在数据库中也创建表t_user,字段只有id和username即可
package org.example.myredis.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
/**
* @author xiajunfeng
*/
@Data
@TableName("t_user")
public class User implements Serializable {
@TableId(type = IdType.AUTO)
private Integer id;
@TableField
private String username;
}
UserController
package org.example.myredis.controller;
import jakarta.annotation.Resource;
import org.example.myredis.entity.User;
import org.example.myredis.service.UserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* UserController
* @author xiajunfeng
* @date 2025/03/09
*/
@RestController
public class UserController {
@Resource
private UserService service;
@GetMapping(value = "/user/{id}")
public User getUserById(@PathVariable("id") Integer id) {
return service.getUserById(id);
}
}
UserService
package org.example.myredis.service;
import org.example.myredis.entity.User;
/**
* @author xiajunfeng
*/
public interface UserService {
User getUserById(Integer id);
}
UserServiceImpl:注意这里使用了自定义注解@MyRedisCache
package org.example.myredis.service.impl;
import jakarta.annotation.Resource;
import org.example.myredis.annotation.MyRedisCache;
import org.example.myredis.entity.User;
import org.example.myredis.mapper.UserMapper;
import org.example.myredis.service.UserService;
import org.springframework.stereotype.Service;
/**
* UserServiceImpl
* @author xiajunfeng
* @date 2025/03/09
*/
@Service
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
@MyRedisCache(keyPrefix = "user",matchValue = "#id")
public User getUserById(Integer id) {
return userMapper.selectById(id);
}
}
UserMapper
package org.example.myredis.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.example.myredis.entity.User;
/**
* @author xiajunfeng
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
自定义注解MyRedisCache
package org.example.myredis.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* MyRedisCache
* @author xiajunfeng
* @date 2025/03/09
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRedisCache {
String keyPrefix();
String matchValue();
}
最后一个就是关键的AOP:MyRedisCacheAspect
package org.example.myredis.aop;
import jakarta.annotation.Resource;
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.example.myredis.annotation.MyRedisCache;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.expression.EvaluationContext;
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.lang.reflect.Method;
/**
* MyRedisCacheAspect
* @author xiajunfeng
* @date 2025/03/09
*/
@Component
@Aspect
public class MyRedisCacheAspect {
@Resource
private RedisTemplate redisTemplate;
@Around("@annotation(org.example.myredis.annotation.MyRedisCache)")
public Object doCache(ProceedingJoinPoint joinPoint) {
Object result = null;
try {
//1 获得重载后的方法名
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
Method method = signature.getMethod();
//2 确定方法名后获得该方法上面配置的注解标签MyRedisCache
MyRedisCache myRedisCacheAnnotation = method.getAnnotation(MyRedisCache.class);
//3 拿到了MyRedisCache这个注解标签,获得该注解上面配置的参数进行封装和调用
String keyPrefix = myRedisCacheAnnotation.keyPrefix();
String matchValueSpringEL = myRedisCacheAnnotation.matchValue();
//4 SpringEL 解析器
ExpressionParser parser = new SpelExpressionParser();
//#id
Expression expression = parser.parseExpression(matchValueSpringEL);
EvaluationContext context = new StandardEvaluationContext();
//5 获得方法里面的形参个数
Object[] args = joinPoint.getArgs();
DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
String[] parameterNames = discoverer.getParameterNames(method);
for (int i = 0; i < parameterNames.length; i++) {
System.out.println("获得方法里参数名和值: " + parameterNames[i] + "\t" + args[i].toString());
context.setVariable(parameterNames[i], args[i].toString());
}
//6 通过上述,拼接redis的最终key形式
String key = keyPrefix + ":" + expression.getValue(context);
System.out.println("------拼接redis的最终key形式: " + key);
//7 先去redis里面查询看有没有
result = redisTemplate.opsForValue().get(key);
if (result != null) {
System.out.println("------redis里面有,我直接返回结果不再打扰mysql: " + result);
return result;
}
//8 redis里面没有,去找mysql查询或叫进行后续业务逻辑
//主业务逻辑查询mysql,放行
result = joinPoint.proceed();
//9 mysql步骤结束,还需要把结果存入redis一次,缓存补偿
if (result != null) {
System.out.println("------redis里面无,还需要把结果存入redis一次,缓存补偿: " + result);
redisTemplate.opsForValue().set(key, result);
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return result;
}
}
最终测试:在数据库t_user表中添加id为1的数据,启动后端服务,访问localhost:8081/user/1,可在浏览器看到返回的User Json,同时控制台也能看到我们输出的相关文字,同时在Redis中也能找到id为1的user数据