Redis7之Spring Boot集成Redis

Posted 晓风残月Lx

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis7之Spring Boot集成Redis相关的知识,希望对你有一定的参考价值。

十一 Spring Boot集成Redis

1.配置文件

redis.conf配置文件,改完后确保生效,记得重启,记得重启

  • 默认daemonize no 改为 daemonize yes

    • 默认protected-mode yes 改为 protected-mode no

    • 默认bind 127.0.0.1 改为 直接注释掉(默认bind 127.0.0.1只能本机访问)或改成本机IP地址,否则影响远程IP连接

    • 添加redis密码 改为 requirepass 你自己设置的密码

2.防火墙

启动: systemctl start firewalld
关闭: systemctl stop firewalld
查看状态: systemctl status firewalld 
开机禁用  : systemctl disable firewalld
开机启用  : systemctl enable firewalld    
添加 :firewall-cmd --zone=public --add-port=80/tcp --permanent    (--permanent永久生效,没有此参数重启后失效)
重新载入: firewall-cmd --reload
查看: firewall-cmd --zone= public --query-port=80/tcp
删除: firewall-cmd --zone= public --remove-port=80/tcp --permanent

3.Jedis (一般不用了,了解即可)

1.介绍

Jedis Client 是Redis 官网推荐的一个面向 Java 客户端,库文件实现了对各类API进行封装调用

2.步骤

  1. 建Moudle redis_7_study

  2. 改POM

    <?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.atguigu.redis7</groupId>
        <artifactId>redis7_study</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.6.10</version>
            <relativePath/>
        </parent>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
            <maven.compiler.source>1.8</maven.compiler.source>
            <maven.compiler.target>1.8</maven.compiler.target>
            <junit.version>4.12</junit.version>
            <log4j.version>1.2.17</log4j.version>
            <lombok.version>1.16.18</lombok.version>
        </properties>
    
        <dependencies>
            <!--SpringBoot通用依赖模块-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <!--jedis-->
            <dependency>
                <groupId>redis.clients</groupId>
                <artifactId>jedis</artifactId>
                <version>4.3.1</version>
            </dependency>
            <!--通用基础配置-->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>$junit.version</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>$log4j.version</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>$lombok.version</version>
                <optional>true</optional>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    
  3. 写YML

    server.port=7777
    
    spring.application.name=redis7_study
    
  4. 主启动

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class Redis7Study01Application 
    
        public static void main(String[] args) 
            SpringApplication.run(Redis7Study01Application.class, args);
        
    
    
  5. 业务类

    import redis.clients.jedis.Jedis;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    
    /**
     * @Author:晓风残月Lx
     * @Date: 2023/3/13 21:32
     */
    public class JedisDemo 
        public static void main(String[] args) 
            // 1 connection 连接,通过指定ip和端口号
            Jedis jedis = new Jedis("192.168.238.111", 6379);
    
            // 2 指定访问服务器密码
            jedis.auth("123456");
    
            //  3 获得了Jedis客户端,可以像jdbc一样访问redis
            System.out.println(jedis.ping());
    
            // keys
            Set<String> keys = jedis.keys("*");
            System.out.println(keys);
    
            // string
            jedis.set("k3","hello-jedis");
            System.out.println(jedis.get("k3"));
            System.out.println(jedis.ttl("k3"));
    
            // list
            jedis.lpush("list","11","22","33");
            List<String> list = jedis.lrange("list", 0, -1);
            for (String s : list) 
                System.out.println(s);
            
            System.out.println(jedis.rpop("list"));
            System.out.println(jedis.lpop("list"));
    
            // hash
            jedis.hset("hset1","k1","v1");
            Map<String,String> hash = new HashMap<>();
            hash.put("k1","1");
            hash.put("k2","2");
            hash.put("k3","3");
            jedis.hmset("hset2",hash);
            System.out.println(jedis.hmget("hset2","k1","k3","k2"));
            System.out.println(jedis.hget("hset1", "k1"));
            System.out.println(jedis.hexists("hset2","k2"));
            System.out.println(jedis.hkeys("hset2"));
    
            // set
            jedis.sadd("set1","1","2","3");
            jedis.sadd("set2","4");
            System.out.println(jedis.smembers("set1"));
            System.out.println(jedis.scard("set1"));
            System.out.println(jedis.spop("set1"));
            jedis.smove("set1","set2","1");
            System.out.println(jedis.smembers("set1"));
            System.out.println(jedis.smembers("set2"));
            System.out.println(jedis.sinter("set1", "set2"));  // 交集
            System.out.println(jedis.sunion("set1","set2"));   // 并集
    
            // zset
            jedis.zadd("zset1",100,"v1");
            jedis.zadd("zset1",80,"v2");
            jedis.zadd("zset1",60,"v3");
    
            List<String> zset1 = jedis.zrange("zset1", 0, -1);
            for (String s : zset1) 
                System.out.println(s);
            
            List<String> zset11 = jedis.zrevrange("zset1", 0, -1);
            for (String s : zset11) 
                System.out.println(s);
            
        
    
    

4.Lettuce

1.介绍以及和Jedis的区别

Lettuce是一个Redis的Java驱动包,Lettuce翻译为生菜,没错,就是吃的那种生菜,所以它的Logo就是生菜

2.步骤

  1. 改POM(导包)

     <!--lettuce-->
            <dependency>
                <groupId>io.lettuce</groupId>
                <artifactId>lettuce-core</artifactId>
                <version>6.2.1.RELEASE</version>
            </dependency>
    
  2. 业务类

    import io.lettuce.core.RedisClient;
    import io.lettuce.core.RedisURI;
    import io.lettuce.core.api.StatefulRedisConnection;
    import io.lettuce.core.api.sync.RedisCommands;
    
    import java.util.List;
    
    /**
     * @Author:晓风残月Lx
     * @Date: 2023/3/13 22:13
     */
    public class LettuceDemo 
    
        public static void main(String[] args) 
            // 1 使用构建器链式编程来builder我们的RedisURI
            RedisURI uri = RedisURI.builder()
                    .withHost("192.168.238.111")
                    .withPort(6379)
                    .withAuthentication("default", "123456")
                    .build();
            // 2 连接客户端
            RedisClient redisClient = RedisClient.create(uri);
            StatefulRedisConnection<String, String> conn = redisClient.connect();
    
            // 3 创建操作的command, 通过conn 创建
            RedisCommands<String, String> commands = conn.sync();
    
            // string
            commands.set("k1","v1");
            System.out.println("==========================="+commands.get("k1"));
            System.out.println("==========================="+commands.mget("k1","k2"));
            List<String> keys = commands.keys("*");
            for (String key : keys) 
                System.out.println("========================="+key);
            
    
            // list
            commands.lpush("list01","1","2","3");
            List<String> list01 = commands.lrange("list01", 0, -1);
            for (String s : list01) 
                System.out.println("================"+s);
            
            System.out.println("===================="+ commands.rpop("list01", 2));
    
            // hash
            commands.hset("hash","k1","v1");
            commands.hset("hash","k2","v2");
            commands.hset("hash","k3","v3");
            System.out.println("======================="+commands.hgetall("hash"));
            Boolean hexists = commands.hexists("hash", "v2");
            System.out.println("------"+hexists);
    
            // set
            commands.sadd("s1","1","2");
            System.out.println("=================================" + commands.smembers("s1"));
            System.out.println(commands.sismember("s1", "1"));
            System.out.println(commands.scard("s1"));
    
            // zset
            commands.zadd("a1",100,"v1");
            commands.zadd("a1",80,"v2");
            System.out.println(commands.zrange("a1", 0, -1))5分钟入手Spring Boot;
  3. Spring Boot数据库交互之Spring Data JPA;
  4. Spring Boot数据库交互之Mybatis;
  5. Spring Boot视图技术;
  6. Spring Boot之整合Swagger;
  7. Spring Boot之junit单元测试踩坑;
  8. 如何在Spring Boot中使用TestNG;
  9. Spring Boot之整合logback日志;
  10. Spring Boot之整合Spring Batch:批处理与任务调度;
  11. Spring Boot之整合Spring Security: 访问认证;
  12. Spring Boot之整合Spring Security: 授权管理;
  13. Spring Boot之多数据库源:极简方案;
  14. Spring Boot之使用MongoDB数据库源;
  15. Spring Boot之多线程、异步:@Async;
  16. Spring Boot之前后端分离(一):Vue前端;
  17. Spring Boot之前后端分离(二):后端、前后端集成;
  18. Spring Boot之前后端分离(三):登录、登出、页面认证;
  19. Spring Boot之面向切面编程:Spring AOP;
  20. Spring Boot之集成Redis(一):Redis初入门;
  21. Spring Boot之集成Redis(二):集成Redis;

在上一篇文章Spring Boot之集成Redis(二):集成Redis中,我们一起学习了如何使用StringRedisTemplate来与Redis进行交互,但也在文末提到这种方式整体代码还是比较多的,略显臃肿,并剧透了另外一种操作Redis的方式:

基于Spring Cache方式操作Redis!

今天的内容厉害啦,不仅能学会在Spring Boot中更好的使用Redis,还能学习Spring Cache!

我们一起拨开云雾睹青天吧!

代码基于上期使用的Git Hub仓库演进,欢迎取阅:

整体步骤

  1. 安装commons-pool2依赖;
  2. 修改application.properties配置;
  3. 学习Spring Cache的缓存注解;
  4. 使用Spring Cache的缓存注解操作Redis;
  5. Spring Cache + Redis 缓存演示;
  6. 配置类方式配置和管理Redis缓存;
  7. 动态缓存有效期的实现;

1. 安装commons-pool2依赖;

由于我们会在application.properties配置文件中配置lettuce类型的redis连接池,因此需要引入新的依赖:

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-pool2</artifactId>
</dependency>
记得安装一下依赖:
mvn install -Dmaven.test.skip=true -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true

2. 修改application.properties配置;

application.properties文件整体样子:

server.port=8080
# 设置Spring Cache的缓存类型为redis
spring.cache.type=redis
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务端的host和port
spring.redis.host=127.0.0.1
spring.redis.port=6379
# Redis服务端的密码
spring.redis.password=Redis!123
# Redis最大连接数
spring.redis.pool.max-active=8
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
# Redis连接超时时间,单位 ms(毫秒)
spring.redis.timeout=5000

特别是spring.cache.type=redis这个配置,指定了redis作为Spring Cache的缓存(还有多种可选的缓存方式,如Simple、none、Generic、JCache、EhCache、Hazelcast等,请读者有需要自行脑补哈)。

3. 学习Spring Cache的缓存注解;

Spring Cache提供了5个缓存注解,通常直接使用在Service类上,各有其作用:

1. @Cacheable;

@Cacheable 作用在方法上,触发缓存读取操作。
被作用的方法如果缓存中没有,比如第一次调用,则会执行方法体,执行后就会进行缓存,而如果已有缓存,那么则不再执行方法体;

2. @CachePut;

@CachePut 作用在方法上,触发缓存更新操作;
被作用的方法每次调用都会执行方法体;

3. @CacheEvict;

@CachePut 作用在方法上,触发缓存失效操作;
也即清除缓存,被作用的方法每次调用都会执行方法体,并且使用方法体返回更新缓存;

4. @Caching;

@Caching作用在方法上,注解中混合使用@Cacheable、@CachePut、@CacheEvict操作,完成复杂、多种缓存操作;
比如同一个方法,关联多个缓存,并且其缓存名字、缓存key都不同时,或同一个方法有增删改查的缓存操作等,被作用的方法每次调用都会执行方法体

5. @CacheConfig;

在类上设置当前缓存的一些公共设置,也即类级别的全局缓存配置。

4. 使用Spring Cache的缓存注解操作Redis;

整体步骤:

1). 修改项目入口类;
2). 编写演示用实体类;
3). 编写演示用Service类;
4). 编写演示用Controller类;

1). 修改项目入口类;

在Spring Boot项目入口类App.java上添加注解:@EnableCaching,App.java类整体如下:

package com.github.dylanz666;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

/**
 * @author : dylanz
 * @since : 10/28/2020
 */
@SpringBootApplication
@EnableCaching
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}
2). 编写演示用实体类;

在domain包内建立演示用实体类User.java,简单撸点代码演示一下:

package com.github.dylanz666.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;

import java.io.Serializable;

/**
 * @author : dylanz
 * @since : 10/31/2020
 */
@NoArgsConstructor
@AllArgsConstructor
@Data
@Component
public class User implements Serializable {
    private static final long serialVersionUID = 1L;

    private String userName;
    private String roleName;

    @Override
    public String toString() {
        return "{" +
                ""userName":"" + userName + ""," +
                ""roleName":" + roleName + "" +
                "}";
    }
}
3). 编写演示用Service类;

重点来了,在service包内新建UserService类,编写带有Spring Cache注解的方法们,代码如下:

package com.github.dylanz666.service;

import com.github.dylanz666.domain.User;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;

/**
 * @author : dylanz
 * @since : 10/31/2020
 */
@Service
public class UserService {

    @Cacheable(value = "content", key = "‘id_‘+#userName")
    public User getUserByName(String userName) {
        System.out.println("io operation : getUserByName()");

        //模拟从数据库中获取数据
        User userInDb = new User();
        userInDb.setUserName("dylanz");
        userInDb.setRoleName("adminUser");

        return userInDb;
    }

    @CachePut(value = "content", key = "‘id_‘+#user.userName")
    public User updateUser(User user) {
        System.out.println("io operation : updateUser()");

        //模拟从数据库中获取数据
        User userInDb = new User();
        userInDb.setUserName(user.getUserName());
        userInDb.setRoleName(user.getRoleName());

        return userInDb;
    }

    @CacheEvict(value = "content", key = "‘id_‘+#user.userName")
    public void deleteUser(User user) {
        System.out.println("io operation : deleteUser()");
    }

    @Caching(cacheable = {
            @Cacheable(cacheNames = "test", key = "‘id_‘+#userName")
    }, put = {
            @CachePut(value = "testGroup", key = "‘id_‘+#userName"),
            @CachePut(value = "user", key = "#userName")
    })
    public User getUserByNameAndDoMultiCaching(String userName) {
        System.out.println("io operation : getUserByNameAndDoMultiCaching()");

        //模拟从数据库中获取数据
        User userInDb = new User();
        userInDb.setUserName("dylanz");
        userInDb.setRoleName("adminUser");

        return userInDb;
    }
}

简单介绍一下:

  • 在UserService的方法内模拟了一下数据库中的数据,用于模拟从数据库中获取数据,代码为:
//模拟从数据库中获取数据
User userInDb = new User();
userInDb.setUserName("dylanz");
userInDb.setRoleName("adminUser");
  • 我写了4个方法getUserByName(String userName)、
    updateUser(User user)、deleteUser(User user)、getUserByNameAndDoMultiCaching(String userName)分别用于演示查询缓存、更新缓存、删除缓存,以及同一个方法使用混合缓存注解;

  • 缓存注解中使用了2个基本属性(还有其他属性),value(也可以用cacheNames)和key,key如果是从方法体获取的,则需要在key前加#号,比如key="#userName"或者key = "‘id_‘+#userName",当然也可以写死如key="test123";

  • 缓存注解中的key属性值可以从对象中获取,如:key = "‘id_‘+#user.userName";

  • 缓存注解的value(或cacheNames)属性、key属性共同构成了Redis服务端中完整的key,如value = "content", key = "test123",则在Redis服务端,其完整key为:content::test123;

  • @Caching中可混合使用缓存注解@Cacheable、@CachePut、@CacheEvict,对应的属性是:cacheable、put、evict,复杂场景有帮助!

3). 编写演示用Controller类;
在controller包内新建UserController.java类,编写4个API,分别使用Service中的4个带有Spring Cache注解的方法们,代码如下:

package com.github.dylanz666.controller;

import com.github.dylanz666.domain.User;
import com.github.dylanz666.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * @author : dylanz
 * @since : 10/31/2020
 */
@RestController
@RequestMapping("/api/user")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("")
    public @ResponseBody User getUserByName(@RequestParam String userName) {
        return userService.getUserByName(userName);
    }

    @PutMapping("")
    public @ResponseBody
    User updateUser(@RequestBody User user) {
        return userService.updateUser(user);
    }

    @DeleteMapping("")
    public @ResponseBody String deleteUser(@RequestBody User user) {
        userService.deleteUser(user);

        return "success";
    }

    @GetMapping("/multiCaching")
    public @ResponseBody User getUserByNameAndDoMultiCaching(@RequestParam String userName) {
        return userService.getUserByNameAndDoMultiCaching(userName);
    }
}

至此,项目整体结构:

技术图片
项目整体结构

5. Spring Cache + Redis 缓存演示;

1). 启动Redis服务端;
redis-server.exe redis.windows.conf
技术图片
启动Redis服务端
2). 启动Redis客户端,进入monitor模式;

启动:

redis-cli.exe -h 127.0.0.1 - p 6379
auth Redis!123

在Redis客户端,进入monitor模式,命令:

monitor
技术图片
进入monitor模式
3). 启动项目;
技术图片
启动项目
4). postman访问Controller中的API;
技术图片
调用API 1 - postman
技术图片
调用API 1 - 监控窗口

我们会看到,第一次调用API时,执行了方法体,并完成了第一次Redis缓存!

技术图片
调用API 2 - postman
技术图片
调用API 2 - 监控窗口
  • 更新后,再次调用获取user的API:
技术图片
缓存更新后再次调用API 1 - postman
技术图片
缓存更新后再次调用API 1 - 监控窗口

我们会看到,缓存确实被更新了!

技术图片
调用API 3 - postman
技术图片
调用API 3 - 监控窗口
  • 缓存删除后,再次调用获取user的API:
技术图片
缓存删除后,再次调用获取user的API - postman
技术图片
缓存删除后,再次调用获取user的API - 监控窗口

我们会看到,缓存被删除后,再次获取缓存则会自动再次执行方法体,并完成新一轮的缓存!

技术图片
调用API 4 - postman
技术图片
调用API 4 - 监控窗口

同时为了更直观的让大家看到缓存有起到效果,我在Service层的方法内,都往控制台打印了一些文字,如:io operation : getUserByName()...

我分别各调用了3次具有更新缓存的API、删除缓存的API、获取缓存的API、混合使用缓存注解的API,控制台打印:
技术图片
控制台打印

我们会发现:更新缓存、删除缓存、混合使用缓存注解的缓存,每次都有执行方法体,而获取缓存只执行了一次方法体,这跟我们之前介绍缓存注解的时候是一致的,再次验证了之前对缓存注解的认识!

6. 配置类方式配置和管理Redis缓存;

在config包内创建Redis配置类RedisConfig.java,并编写如下代码:

package com.github.dylanz666.config;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.*;

import java.time.Duration;

/**
 * @author : dylanz
 * @since : 10/31/2020
 */

稍微解读一下:

1). 默认情况下Redis的序列化会使用jdk序列化器JdkSerializationRedisSerializer,而该序列化方式,在存储内容时除了属性的内容外还存了其它内容在里面,总长度长,且不容易阅读,因此我们自定义了序列化策略:redisTemplate(),使用该自定义序列化方式后,存储在Redis的内容就比较容易阅读了!

技术图片
配置前:不容易阅读
技术图片
配置后:容易阅读

2). Spring Cache注解方式,默认是没有过期概念的,即设置了缓存,则一直有效,这显然不能完全满足使用要求,而RedisConfig中的CacheManager给我们提供了一种方式来设置缓存过期策略!

3). cacheManager()用于配置缓存管理器,我们在方法内配置了一个entryTtl为1分钟,TTL即Time To Live,代表缓存可以生存多久,我没有在cacheManager()方法内指明哪个缓存,因此该TTL是针对所有缓存的,是全局性的,也是我们设置的默认TTL;

4). 我们可以给不同的key设置不同的TTL,但hard code的缓存key就有点多啦,此处不介绍啦!

5). 该配置类使用了注解@EnableCaching,则Spring Boot入口类App.java中的@EnableCaching注解就可以删除啦!

7. 动态缓存有效期的实现;

动态缓存有效期的实现有多种方式,如:
1). 在缓存注解上使用不同的自定义的CacheManager;
2). 自定义Redis缓存有效期注解;
3). 利用在Spring Cache缓存注解中value属性中设置的值,设置缓存有效期;

1). 在缓存注解上使用不同的自定义的CacheManager;

在第6步中,我们使用了一个全局的CacheManager,其实这个可以更灵活,可以用于给不同的缓存设置不同的TTL,也即缓存过期。

我们可以在RedisConfig.java中编写多个不同名字的CacheManager,每个CacheManager使用不同的TTL。

然后缓存注解中使用不同的CacheManager,就能达到不同缓存有不同TTL的目的啦,并且没有在CacheManager中hard code缓存key,演示代码:

  • RedisConfig.java演示代码:
package com.github.dylanz666.config;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.*;

import java.time.Duration;

/**
 * @author : dylanz
 * @since : 10/31/2020
 */

注意:当有多个CacheManager时,要设置一个默认的CacheManager,即在默认的CacheManager上添加@Primary注解,否则启动项目会报错!

  • 在想演示的缓存注解上添加cacheManger属性,并指定RedisConfig.java中的CacheManager,如:
@CachePut(value = "content", key = "‘id_‘+#user.userName", cacheManager = "cacheManager10s")
public User updateUser(User user) {
    System.out.println("io operation : updateUser()");

    //模拟从数据库中获取数据
    User userInDb = new User();
    userInDb.setUserName(user.getUserName());
    userInDb.setRoleName(user.getRoleName());

    return userInDb;
}

那么,使用了cacheManager10s的缓存,就会按我们的设置,只缓存10秒钟,其他没设置cacheManager的缓存,则会缓存1分钟,亲测有效!

自定义cacheManager的方式提供了一定的灵活性,但当缓存有效期越来越多样时,RedisConfig.java中的代码就会越来越多,越来越难以管理。接下来介绍另外一种更为灵活的方式!

2). 自定义Redis缓存有效期注解;

这种解题思路是:自定义Redis缓存有效期注解,通过切面编程,在切面中完成Redis缓存有效期的设置。

关于切面编程,可以参考之前文章:Spring Boot之面向切面编程:Spring AOP

(1). 编写时间单位枚举类;
出于灵活性考虑,我将在自定义的注解中使用2个属性,一个为duration(时间大小)属性,一个为unit(时间单位)属性,duration与unit结合使用,可以灵活的设置缓存的有效期!
项目内新建constant包,在包内新建时间单位枚举类,编写代码如下:

package com.github.dylanz666.constant;

/**
 * @author : dylanz
 * @since : 11/02/2020
 */
public enum DurationUnitEnum {
    NANO,
    MILLI,
    SECOND,
    MINUTE,
    HOUR,
    DAY
}

(2). 自定义注解;
项目内新建annotation包,在包内新建注解类Ttl.java(名字随意),编写注解类:

package com.github.dylanz666.annotation;

import com.github.dylanz666.constant.DurationUnitEnum;

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

/**
 * @author : dylanz
 * @since : 11/01/2020
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Ttl {
    //设置缓存有效时间,单位为秒,默认60秒
    long duration() default 60;

    //时间单位,默认为秒,SECOND,可选的有:NANO,MILLI,SECOND,MINUTE,HOUR,DAY
    DurationUnitEnum unit() default DurationUnitEnum.SECOND;
}

(3). 创建切面编程类,并完成自定义注解的逻辑实现;
经过实验,如果没有在切面中对Spring Cache的缓存注解、自定义的动态缓存有效期注解进行排序,则可能出现自定义的注解先执行,Spring Cache的缓存注解后执行,导致缓存有效期被默认的TTL覆盖,即:自定义的注解没起到作用!

因此,需要分别对Spring Cache的缓存注解、自定义的动态缓存有效期注解编写切面类,并使用@Order进行切面排序!

在config包内新建2个切面类:AOPConfig.java和
AOPConfig2.java。

AOPConfig.java代码:
package com.github.dylanz666.config;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;

/**
 * @author : dylanz
 * @since : 11/02/2020
 */
@Configuration
@Aspect
@Order(1)
public class AOPConfig {
    @Around("@annotation(org.springframework.cache.annotation.Cacheable)")
    public Object cacheableAop(final ProceedingJoinPoint joinPoint) throws Throwable {
        return joinPoint.proceed();
    }

    @Around("@annotation(org.springframework.cache.annotation.CachePut)")
    public Object cachePutAop(final ProceedingJoinPoint joinPoint) throws Throwable {
        return joinPoint.proceed();
    }

    @Around("@annotation(org.springframework.cache.annotation.CacheEvict)")
    public Object cacheEvictAop(final ProceedingJoinPoint joinPoint) throws Throwable {
        return joinPoint.proceed();
    }

    @Around("@annotation(org.springframework.cache.annotation.Caching)")
    public Object cachingAop(final ProceedingJoinPoint joinPoint) throws Throwable {
        return joinPoint.proceed();
    }
}
AOPConfig2.java代码:
package com.github.dylanz666.config;

import com.github.dylanz666.annotation.Ttl;
import com.github.dylanz666.constant.DurationUnitEnum;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.StringRedisTemplate;
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 java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.time.Duration;

/**
 * @author : dylanz
 * @since : 11/01/2020
 */
@Configuration
@Aspect
@Order(2)
public class AOPConfig2 {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @After("@annotation(com.github.dylanz666.annotation.Ttl)")
    public void simpleAop2(final JoinPoint joinPoint) throws Throwable {
        //获取一些基础信息
        String methodName = joinPoint.getSignature().getName();
        Class<?> targetClass = joinPoint.getTarget().getClass();
        Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getParameterTypes();
        Method objMethod = targetClass.getMethod(methodName, parameterTypes);

        //获取想要设置的缓存有效时长
        Ttl cacheDuration = objMethod.getDeclaredAnnotation(Ttl.class);
        long duration = cacheDuration.duration();
        DurationUnitEnum unit = cacheDuration.unit();

        //获取缓存注解的values/cacheNames和key
        String[] values = getValues(objMethod);
        String key = getKey(objMethod);

        //准备好变量名与变量值的上下文绑定
        Parameter[] parameters = objMethod.getParameters();
        Object[] args = joinPoint.getArgs();
        StandardEvaluationContext ctx = new StandardEvaluationContext();
        for (int i = 0; i < parameters.length; i++) {
            ctx.setVariable(parameters[i].getName(), args[i]);
        }

        //获取真实的key,而不是变量形式的key
        ExpressionParser expressionParser = new SpelExpressionParser();
        Expression expression = expressionParser.parseExpression(key);
        String realKey = expression.getValue(ctx, String.class);

        //设置缓存生效时长,即过期时间
        setExpiration(values, realKey, duration, unit);
    }

    private void setExpiration(String[] values, String realKey, long duration, DurationUnitEnum unit) throws InterruptedException {
        Duration expiration;
        if (unit == DurationUnitEnum.NANO) {
            expiration = Duration.ofNanos(duration);
        } else if (unit == DurationUnitEnum.MILLI) {
            expiration = Duration.ofMillis(duration);
        } else if (unit == DurationUnitEnum.MINUTE) {
            expiration = Duration.ofMinutes(duration);
        } else if (unit == DurationUnitEnum.HOUR) {
            expiration = Duration.ofHours(duration);
        } else if (unit == DurationUnitEnum.DAY) {
            expiration = Duration.ofDays(duration);
        } else {
            expiration = Duration.ofSeconds(duration);
        }
        for (String value : values) {
            String redisKey = value + "::" + realKey;
            stringRedisTemplate.expire(redisKey, expiration);
        }
    }

    private String[] getValues(Method objMethod) {
        Cacheable cacheable = objMethod.getDeclaredAnnotation(Cacheable.class);
        CachePut cachePut = objMethod.getDeclaredAnnotation(CachePut.class);
        CacheEvict cacheEvict = objMethod.getDeclaredAnnotation(CacheEvict.class);
        //value
        if (cacheable != null && cacheable.value().length > 0) {
            return cacheable.value();
        }
        if (cachePut != null && cachePut.value().length > 0) {
            return cachePut.value();
        }
        if (cacheEvict != null && cacheEvict.value().length > 0) {
            return cacheEvict.value();
        }
        //cacheNames
        if (cacheable != null && cacheable.cacheNames().length > 0) {
            return cacheable.cacheNames();
        }
        if (cachePut != null && cachePut.cacheNames().length > 0) {
            return cachePut.cacheNames();
        }
        if (cacheEvict != null && cacheEvict.cacheNames().length > 0) {
            return cacheEvict.cacheNames();
        }
        return new String[]{};
    }

    private String getKey(Method objMethod) {
        Cacheable cacheable = objMethod.getDeclaredAnnotation(Cacheable.class);
        CachePut cachePut = objMethod.getDeclaredAnnotation(CachePut.class);
        CacheEvict cacheEvict = objMethod.getDeclaredAnnotation(CacheEvict.class);
        if (cacheable != null && !cacheable.key().equals("")) {
            return cacheable.key();
        }
        if (cachePut != null && !cachePut.key().equals("")) {
            return cachePut.key();
        }
        if (cacheEvict != null && !cacheEvict.key().equals("")) {
            return cacheEvict.key();
        }
        return "";
    }
}

由于@Caching比较复杂,我们没有将Ttl.java应用到@Caching注解哦,读者可自行探索!

(3). 使用自定义的缓存有效期注解;
只需要在有使用@Cacheable、@CachePut、@CacheEvict注解的方法上再添加使用@Ttl注解即可,如:

@Cacheable(value = "content", key = "‘id_‘+#userName")
@Ttl(duration = 10, unit = DurationUnitEnum.SECOND)
public User getUserByName(String userName) {
    System.out.println("io operation : getUserByName()");

    //模拟从数据库中获取数据
    User userInDb = new User();
    userInDb.setUserName("dylanz");
    userInDb.setRoleName("adminUser");

    return userInDb;
}

和:

@CachePut(value = "content", key = "‘id_‘+#user.userName")
@Ttl(duration = 1, unit = DurationUnitEnum.HOUR)
public User updateUser(User user) {
    System.out.println("io operation : updateUser()");

    //模拟从数据库中获取数据
    User userInDb = new User();
    userInDb.setUserName(user.getUserName());
    userInDb.setRoleName(user.getRoleName());

    return userInDb;
}

注:如果@Ttl注解内没有duration,则默认为60,如果unit单位没有,则默认为秒,如:

  • @Ttl:代表缓存60秒;
  • @Ttl(duration = 10):代表缓存10秒;
  • @Ttl(duration = 10,DurationUnitEnum.MINUTE):代表缓存10分钟;

大家可以根据自己需要再进行调整。

至此项目整体结构:

技术图片
项目整体结构

演示一下:

技术图片
动态缓存有效期演示

同时我们也能在API的返回中,看到缓存失效的效果!

3). 利用在Spring Cache缓存注解中value属性中设置的值,设置缓存有效期;

这种方式同样可以利用切面编程,在获取到value属性后,截取我们想设置的缓存有效期时长,然后设置缓存有效期。

例如,我们有一个Spring Cache缓存注解:

@CacheEvict(value = "content#3600", key = "‘id_‘+#user.userName")

参照自定义注解的代码,我们可以很轻松获取到value为 "content#3600",然后可以截取3600为缓存有效期时长,而单位可以默认设置为秒(根据实际要求设置),最后还是一样去设置redis key的expiration。整体与自定义注解非常像,我们就不做过多介绍啦!

也有使用自定义的RedisCacheManager来实现的,未来有机会再深入哈!


上述3种方式都可以完成动态缓存有效期的设置,而我最喜欢的莫过于:自定义注解的方式,该方式灵活、易于理解,用户编码时也非常简单!

至此Spring Cache + Redis完成灵活的Redis操作到此告一段落啦,Spring Boot项目中使用Redis缓存再上层楼,有没有Get到?

如果本文对您有帮助,麻烦点赞、关注!

谢谢!























以上是关于Redis7之Spring Boot集成Redis的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot 集成 Redis

Spring Boot学习笔记——Spring Boot与Redis的集成

spring boot 集成 Redis

7Spring Boot 与 Redis 集成

Spring Boot集成Spring Cache 和 Redis

spring boot 常见的第三方集成