在 Spring Boot 中整合、使用 Redis

Redis 是一款开源的,使用 C 开发的高性能内存 Key/Value 数据库,支持 String、Set、Hash、List、Stream 等等数据类型。它被广泛用于缓存、消息队列、实时分析、计数器和排行榜等场景。基本上是当代应用中必不可少的软件!

Spring Boot 对 Redis 提供了开箱即用的组件:spring-boot-starter-data-redis。通过这个 starter,我们只需要几行简单的配置就可以快速地在 Spring Boot 中整合、使用 Redis。

Spring Boot 整合 Redis

Maven 依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

除了 spring-boot-starter-data-redis 外,还添加了 commons-pool2 依赖,是因为我们需要使用到连接池。

配置属性

只需要在 application.yaml | properties 中配置如下常用的基本属性即可:

spring:
  data:
    redis:
      # 连接地址
      host: "localhost"
      # 端口
      port: 6379
      # 数据库
      database: 0
      # 用户名,如果有
      # username:
      # 密码,如果有
      # password:
      # 连接超时
      connect-timeout: 5s
      # 读超时
      timeout: 5s

      # Lettuce 客户端的配置
      lettuce:
        # 连接池配置
        pool:
          # 最小空闲连接
          min-idle: 0
          # 最大空闲连接
          max-idle: 8
          # 最大活跃连接
          max-active: 8
          # 从连接池获取连接 最大超时时间,小于等于0则表示不会超时
          max-wait: -1ms

注意,如果你使用的是 spring boot 2.x,上述配置的命名空间应该是 spring.redis 而不是 spring.data.redis

更多完整的配置属性,请参阅 官方文档

使用 Jedis 客户端

Spring Data Redis 默认使用 Lettuce 作为 Redis 客户端。官方还对 Jedis 提供了支持,你可以根据你的喜好进行选择。

要替换为 Jedis,首先需要从 spring-boot-starter-data-redis 排除 lettuce ,并且添加 jedis 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

然后修改配置文件,把 lettuce 配置替换为 jedis 配置即可:

spring.data.redis.jedis.pool.enabled=true
spring.data.redis.jedis.pool.max-active=8
spring.data.redis.jedis.pool.max-idle=8
spring.data.redis.jedis.pool.max-wait=-1ms
spring.data.redis.jedis.pool.min-idle=0
spring.data.redis.jedis.pool.time-between-eviction-runs=

得益于 Spring Data Redis 提供的抽象、封装。在修改了底层客户端后,我们基本上不用修改任何业务代码。

推荐在项目中使用 lettuce 客户端,因为它是基于 Netty 开发,支持非阻塞式 IO,性能会更好。

使用 StringRedisTemplate

配置就绪后,StringRedisTemplate 已经可用,你可以在任何地方注入、使用:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class DemoApplicationTests {

    static final Logger logger = LoggerFactory.getLogger(DemoApplicationTests.class);

    // 注入 StringRedisTemplate
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Test
    public void test() {
        
        // 设置
        this.stringRedisTemplate.opsForValue().set("title", "spring 中文网", Duration.ofMinutes(5));

        // 读取
        String val = this.stringRedisTemplate.opsForValue().get("title");

        logger.info("value={}", val);
    }
}

对于 StringRedisTemplate 更完整的方法列表,你可以参阅其 java doc

自定义 RedisTemplate

如果基本的 StringRedisTemplate 不能满足你的需求,你也可以自定义 RedisTemplate 实现。

例如,我们想要自定义一个 JsonRedisTemplate,用于把任意 Java 对象序列化为 json 数据存储到 Redis,并且也能够把 Redis 中的 json 数据反序列化为任意 Java 对象。

如下:

import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;


@Component
public class JsonRedisTemplate extends RedisTemplate<String, Object>{

    public JsonRedisTemplate(RedisConnectionFactory redisConnectionFactory) {

        // 构造函数注入 RedisConnectionFactory,设置到父类
        super.setConnectionFactory(redisConnectionFactory);
        
        // 使用 Jackson 提供的通用 Serializer
        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
        serializer.configure(mapper -> {
            // 如果涉及到对 java.time 类型的序列化,反序列化那么需要注册 JavaTimeModule
            mapper.registerModule(new JavaTimeModule());
        });
        
        // String 类型的 key/value 序列化
        super.setKeySerializer(StringRedisSerializer.UTF_8);
        super.setValueSerializer(serializer);
        
        // Hash 类型的 key/value 序列化
        super.setHashKeySerializer(StringRedisSerializer.UTF_8);
        super.setHashValueSerializer(serializer);
    }
}

首先,继承 RedisTemplate<K,V>,泛型 K 表示 Redis Key 类型,一般都是 String,泛型 V 表示 Redis Value 类型,既然我们需要的是一个通用的 JSON Template,所以设置为 Object,Value 值可以是任意对象。

在构造函数中注入 RedisConnectionFactory 设置到父类,这是必须的

然后创建GenericJackson2JsonRedisSerializer 实例,它是基于 Jackson 的 RedisSerializer 实现,用于任意 Java 对象和 JSON 字符串之间的序列化/反序列化。使用该实例作为普通 Value 和 Hash Value 的序列化/反序列化器。注意,因为序列化的对象可能包含了 java.time 类型的日期字段,如:LocalTimeLocalDate 以及 LocalDateTime,所以需要注册 JavaTimeModule

创建测试类进行测试。如下:

package cn.springdoc.demo.test;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;

import cn.springdoc.demo.redis.JsonRedisTemplate;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class DemoApplicationTests {

    static final Logger logger = LoggerFactory.getLogger(DemoApplicationTests.class);


    // 注入 JsonRedisTemplate
    @Autowired
    JsonRedisTemplate jsonRedisTemplate;

    @SuppressWarnings("unchecked")
    @Test
    public void test() {
        
        // Map
        Map<String, Object> map = new HashMap<>();
        map.put("title", "spring 中文网");
        map.put("url", "https://springdoc.cn");
        map.put("createAt", LocalDateTime.now());
        
        // 设置 key/value
        this.jsonRedisTemplate.opsForValue().set("key1-string", map, Duration.ofMinutes(5));
        // 读取 key/value
        map = (Map<String, Object>) this.jsonRedisTemplate.opsForValue().get("key1-string");
        logger.info("map={}", map);
        
        // 设置 Hash Value
        this.jsonRedisTemplate.opsForHash().put("key2-hash", "app", map);
        // 读取 Hash Value
        map = (Map<String, Object>) this.jsonRedisTemplate.opsForHash().get("key2-hash", "app");

        logger.info("map={}", map);
    }
}

我们创建了一个 Map<String, Object> 对象,存储了 2 个 String 和一个 LocalDateTime 字段。然后使用 JsonRedisTemplate 把它存储为普通 Value 和 Hash Value。

存储成功后,再进行读取,反序列化为原来的 Map<String, Object> 对象。

运行测试,执行日志如下:

[           main] c.s.demo.test.DemoApplicationTests       : map={title=spring 中文网, url=https://springdoc.cn, createAt=2023-09-25T10:53:44.618386900}
[           main] c.s.demo.test.DemoApplicationTests       : map={title=spring 中文网, url=https://springdoc.cn, createAt=2023-09-25T10:53:44.618386900}

如你所见,序列化为 JSON、反序列化为对象都没问题。