自研通用Redis缓存组件

在某个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数据
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值