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序列化器,这种方式存在以下问题:
- 存入Redis的数据不可读,调试和运维困难
- 跨语言兼容性差,其他语言编写的程序无法读取
- 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:*"));