Spring Data Redis

Redis是一个基于Key-Value形式的非关系型内存数据库,主要用作高性能缓存和存储一些临时信息。Java程序中操作Redis数据库有Jedis、Redisson、Lettuce等客户端可以直接使用,而在使用Spring框架时,Spring Data Redis项目提供了RedisTemplate,它为我们进一步封装了Redis客户端,Jedis、Redisson等客户端库都支持集成Spring Data Redis,因此使用Spring Data Redis能让我们在这些客户端之间无缝切换,Spring生态下的很多库也都依赖Spring Data Redis,使用它能起到增强程序代码可移植性的目的。

我们这里以SpringBoot3.5版本为例,介绍Spring Data Redis的用法。

引入Maven依赖

对于SpringBoot环境,集成Spring Data Redis非常简单,只需要引入一个起步依赖即可。

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

spring-boot-starter-data-redis会传递依赖Lettuce客户端,因此一旦添加了该起步依赖,Redis客户端实际也一同被引入了,我们可以直接开始使用。如果我们不想使用Lettuce,仍想使用传统的Jedis或功能更强大的Redisson,一般来说引入对应客户端的SpringBoot起步依赖即可,在项目启动时,Spring Data Redis会根据Bean的注册情况自动根据条件判断使用哪个客户端。有关这些内容可以参考软件工程/Java/第三方库/redisson-分布式内存数据网格相关章节。

对于传统Spring工程,配置Spring Data Redis较为复杂,由于这种情况时至今日已经非常罕见了,这篇笔记已经移除了相关的过时章节,具体可以参考相关文档。

配置Redis客户端

对于使用Lettuce的情况,以下是一个最精简的配置。

application.properties

spring.data.redis.host=127.0.0.1
spring.data.redis.port=6379

Lettuce客户端还有很多其他参数,比如连接的数据库序号、密码,以及集群模式支持等,具体可以参考Lettuce相关章节。

注意:SpringBoot2.x版本中,相关配置前缀为spring.redis,这从SpringBoot3.x开始发生了变化,挪到了spring.data.redis

RedisTemplate存取字符串

默认情况下,Spring Data Redis会向Spring容器中注入RedisTemplate<String, String>这个Bean,我们可以用它在Redis中存取字符串,下面例子代码展示了这种用法。

package com.gacfox.demo;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

@Slf4j
@SpringBootTest
public class RedisOpsTest {
    private final RedisTemplate<String, String> redisTemplate;

    @Autowired
    public RedisOpsTest(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Test
    public void testOpsRedis() {
        ValueOperations<String, String> vops = redisTemplate.opsForValue();
        vops.set("str", "hello, world!");
        String str = vops.get("str");
        log.info("str {}", str);
    }
}

代码非常简单,我们使用Spring的依赖注入机制获得RedisTemplate<String, String>这个Bean,它就是Spring Data Redis中操作Redis的基础工具类。RedisTemplate中有一组方法opsFor*专门用于操作Redis的各种数据结构,我们获取*Operations对象调用其中的方法即可。

存入Redis后,我们可以查看Redis中的数据,可以看到RedisTemplate<String, String>存入的字符串会以原字符串形式存储。

RedisTemplate存取自定义类型

除了字符串,RedisTemplate其实也支持操作任意Java对象,不过这要求Java对象是可序列化的。下面代码我们实例化了一个自定义的Java对象,然后存入Redis再取出。

package com.gacfox.demo;

import com.gacfox.demo.model.Student;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

@Slf4j
@SpringBootTest
public class RedisOpsTest {
    private final RedisTemplate<Object, Object> redisTemplate;

    @Autowired
    public RedisOpsTest(RedisTemplate<Object, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Test
    public void testOpsRedis() {
        ValueOperations<Object, Object> vops = redisTemplate.opsForValue();
        vops.set("tom", Student.builder().name("Tom").age(18).build());
        Student tom = (Student) vops.get("tom");
        if (tom != null) {
            log.info("tom: name={} age={}", tom.getName(), tom.getAge());
        }
    }
}

操作对象时,我们需要依赖注入RedisTemplate<Object, Object>。此外,我们必须为要存储的类实现Serializable接口,RedisTemplate默认使用JDK序列化机制,对象实现Serializable接口后才能够成功进行序列化并存入Redis数据库中,在Redis中序列化后的对象仍使用字符串类型进行存储。除此之外,一个可序列化对象引用其他多个可序列化对象也是能够成功存入Redis中的,总之只要能够成功进行JDK对象序列化就能存入Redis中。

实际上,RedisTemplate<Object, Object>和前面的RedisTemplate<String, String>底层处理方式是有区别的,对于RedisTemplate<Object, Object>,即使操作的是Java字符串类型String,它依然会被JDK序列化。此时查看Redis中的数据,我们可能会看到一些“乱码”,这是正常的,数据的Key和Value都被RedisTemplate<Object, Object>做JDK序列化了,它们实际上已经变成了二进制形式。例如我们向Redis写入str => hello, world!,我们会看到下面情况。

使用keys *查询所有Key。

"\xac\xed\x00\x05t\x00\x03str"

使用get "\xac\xed\x00\x05t\x00\x03str"查询对应Value。

"\xac\xed\x00\x05t\x00\rhello, world!"

不过我们要知道,Redis中并没有二进制类型数据,Redis中字符串类型本身就是二进制安全的(binary-safe),它本质上也只是个字节数组,因此它们实际上还是以Redis中的字符串数据类型存储。

配置序列化器

前面我们可以看到,Spring Data Redis处理自定义Java对象时使用的是JDK序列化器,这种方式存在以下问题:

  1. 存入Redis的数据不可读,调试和运维困难
  2. 跨语言兼容性差,其他语言编写的程序无法读取
  3. JDK序列化机制过于重量级,而且由于历史原因可能存在潜在的序列化机制相关的漏洞

在实际项目中,我们通常更倾向于将Key配置为直接使用传入字符串,Value则使用基于JSON的序列化器GenericJackson2JsonRedisSerializer,下面是具体的配置方式。

package com.gacfox.demo.config;

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.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisTemplateConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        // Key使用String序列化
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);

        // Value使用JSON序列化
        GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);

        template.afterPropertiesSet();
        return template;
    }
}
package com.gacfox.demo;

import com.gacfox.demo.model.Student;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

@Slf4j
@SpringBootTest
public class RedisOpsTest {
    private final RedisTemplate<String, Object> redisTemplate;

    @Autowired
    public RedisOpsTest(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Test
    public void testOpsRedis() {
        ValueOperations<String, Object> vops = redisTemplate.opsForValue();
        vops.set("tom", Student.builder().name("Tom").age(18).build());
        Student tom = (Student) vops.get("tom");
        if (tom != null) {
            log.info("tom: name={} age={}", tom.getName(), tom.getAge());
        }
    }
}

我们使用JavaConfig手动配置了RedisTemplate<String, Object>,并明确设置Key和Value的序列化器,此时在其它地方就可以直接以依赖注入方式获取这个Bean并使用了,其余代码与之前相同。不过注意我们配置了RedisTemplate<String, Object>后,Spring Data Redis默认的内置RedisTemplate<Object, Object>就被覆盖了,我们试图依赖注入RedisTemplate<Object, Object>会报错找不到Bean,但RedisTemplate<String, String>仍能使用,因为它完全是另一个Bean。

此时再查看Redis中的数据,可以看到Key名为tom,Value值类似如下。

{
    "@class": "com.gacfox.demo.model.Student",
    "name": "Tom",
    "age": 18
}

GenericJackson2JsonRedisSerializer序列化的结果是带类型信息的JSON数据,其中@class字段中记录的是Java类型,这样做是为了方便反序列化。使用这种序列化机制就不强制需要实现Java的Serializable接口了,但要求类有无参构造函数和标准的getter/setter方法。

操作Redis数据结构

前面我们说过,RedisTemplate提供了一组opsFor*方法,用于操作Redis中不同的数据结构,这里我们继续详细了解相关方法。

操作字符串(String)

ValueOperations<String, Object> vops = redisTemplate.opsForValue();

// 设置值
vops.set("key", "value");

// 设置值并指定过期时间
vops.set("key", "value", 30, TimeUnit.MINUTES);

// 获取值
Object value = vops.get("key");

// 当Key不存在时才设置(setnx语义)
Boolean success = vops.setIfAbsent("key", "value");

// 自增
vops.increment("counter");
vops.increment("counter", 5);

操作哈希(Hash)

HashOperations<String, String, Object> hops = redisTemplate.opsForHash();

// 设置单个字段
hops.put("user:1", "name", "Tom");
hops.put("user:1", "age", 18);

// 获取单个字段
Object name = hops.get("user:1", "name");

// 设置多个字段
Map<String, Object> fields = new HashMap<>();
fields.put("name", "Tom");
fields.put("age", 18);
hops.putAll("user:1", fields);

// 获取所有字段
Map<String, Object> entries = hops.entries("user:1");

// 删除字段
hops.delete("user:1", "age");

// 判断字段是否存在
Boolean exists = hops.hasKey("user:1", "name");

操作列表(List)

ListOperations<String, Object> lops = redisTemplate.opsForList();

// 从左侧压入
lops.leftPush("list", "a");
lops.leftPushAll("list", "b", "c", "d");

// 从右侧压入
lops.rightPush("list", "e");

// 从左侧弹出
Object value = lops.leftPop("list");

// 从右侧弹出
Object value2 = lops.rightPop("list");

// 按索引范围获取
List<Object> range = lops.range("list", 0, -1);

// 获取列表长度
Long size = lops.size("list");

操作集合(Set)

SetOperations<String, Object> sops = redisTemplate.opsForSet();

// 添加成员
sops.add("set", "a", "b", "c");

// 获取所有成员
Set<Object> members = sops.members("set");

// 判断是否为成员
Boolean isMember = sops.isMember("set", "a");

// 移除成员
sops.remove("set", "a");

// 获取集合大小
Long size = sops.size("set");

// 集合运算
Set<Object> intersect = sops.intersect("set1", "set2"); // 交集
Set<Object> union = sops.union("set1", "set2");         // 并集
Set<Object> diff = sops.difference("set1", "set2");     // 差集

操作有序集合(ZSet)

ZSetOperations<String, Object> zops = redisTemplate.opsForZSet();

// 添加成员及分数
zops.add("zset", "Tom", 90.0);
zops.add("zset", "Jerry", 85.5);
zops.add("zset", "Alice", 95.0);

// 按分数升序获取范围
Set<Object> range = zops.range("zset", 0, -1);

// 按分数降序获取范围
Set<Object> reverseRange = zops.reverseRange("zset", 0, -1);

// 按分数区间获取
Set<Object> byScore = zops.rangeByScore("zset", 80, 95);

// 获取成员分数
Double score = zops.score("zset", "Tom");

// 增加成员分数
zops.incrementScore("zset", "Tom", 5.0);

// 获取成员排名(升序)
Long rank = zops.rank("zset", "Tom");

// 移除成员
zops.remove("zset", "Tom");

操作Key

除了通过opsFor*方法操作具体数据结构外,RedisTemplate本身也提供了一些通用的Key操作方法。

// 删除Key
redisTemplate.delete("key");

// 批量删除
redisTemplate.delete(Arrays.asList("key1", "key2", "key3"));

// 判断Key是否存在
Boolean exists = redisTemplate.hasKey("key");

// 设置过期时间
redisTemplate.expire("key", 30, TimeUnit.MINUTES);

// 获取剩余过期时间
Long ttl = redisTemplate.getExpire("key", TimeUnit.SECONDS);

// 查找匹配的Key(生产环境慎用,推荐使用scan)
Set<String> keys = redisTemplate.keys("user:*");

// 重命名Key
redisTemplate.rename("oldKey", "newKey");

// 获取Key的数据类型
DataType type = redisTemplate.type("key");

消息发布与订阅

Redis提供了发布/订阅(Pub/Sub)消息机制,Spring Data Redis对此也提供了支持。

发布消息例子如下。

redisTemplate.convertAndSend("channel:news", "Hello, subscribers!");

订阅消息时,我们首先需要创建一个消息监听器。

package com.gacfox.demo.listener;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class NewsMessageListener implements MessageListener {
    @Override
    public void onMessage(Message message, byte[] pattern) {
        String channel = new String(message.getChannel());
        String body = new String(message.getBody());
        log.info("Received message from channel [{}]: {}", channel, body);
    }
}

然后还需要在配置类中注册监听容器。

package com.gacfox.demo.config;

import com.gacfox.demo.listener.NewsMessageListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;

@Configuration
public class RedisListenerConfig {
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(
            RedisConnectionFactory connectionFactory,
            NewsMessageListener newsMessageListener) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(newsMessageListener, new ChannelTopic("channel:news"));
        return container;
    }
}

注册监听时,也可以使用PatternTopic进行模式匹配订阅。

container.addMessageListener(newsMessageListener, new PatternTopic("channel:*"));
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。