一、SpringDataRedis简单介绍及引入
SpringData是Spring中数据操作的模块,包括对各种数据库的集成,其中对Redis的集成模块就叫SpringDataRedis
官网地址:https://spring.io/projects/spring-data-redis
1.1 特点:
- 提供了对不同Redis客户端的整合(Lettuce和Jedis)
- 提供了RedisTemplate统一API来操作Redis
- 支持Redis的发布订阅模型
- 支持Redis哨兵和Redis集群
- 支持基于Lettuce的响应式编程
- 支持基于JDK、JSON、字符串、Spring独享的数据序列化及反序列化
- 支持基于Redis的JDKCollection实现
SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同的数据类型的操作API封装到了不同的类型中:
二、SpringDataRedis的使用
2.1 依赖注入
<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>
2.2配置文件
#我们不用再自己写编码了,直接配置就好了
spring:
redis:
host: 127.0.0.1
port: 6379
# database: 0 #默认就是0号库
# password: 有密码的就可以设置 设置密码
lettuce:
pool:
max-active: 8 #最大连接
max-idle: 8 #最大控线连接
min-idle: 0 #最小空闲连接
max-wait: 100 #连接等待时间
RedisTemplate是Spring 自动装配的,在Spring框架载入的时候就自动装配了,所以我们不需要在取额外定义RedisTemplate了。
所以使用时我们只需要自动注入即可
@Autowired
private RedisTemplate redisTemplate
三、RedisTemplate的序列化问题
3.1 问题描述
比如说:
valueOperations.set("city","beijing");
等价于在redis客户端执行
set city "beijing"
但是实际我们去本地redis客户端去查看,发现并不是这样
(注:这里使用的是Another redis destop manager),这是一个可视化redis内容和结构的软件,和navicat一样
这是因为在使用ValueOperations.set的时候,会进行序列化,上述结果实际是存储的是序列化之后的结果。所以即使在客户端使用 get city 也是得不到任何值的
这里的key实际存储的是\xac\xed等。
3.2 问题原因
这是为什么呢?
只是因为默认redisTemplate.opsForValue的set的形参类型都是Object类型
因为SpringDataRedis可以接受任何类型的对象,然后帮我们转化成Redis可以处理的字节
所以我们当我们存入String的值时会被当做了Java对象,注意哦!是java对象,而不是基本的数据类型。
所以你传入String类型,RedisTemplate默认是会将其作为对象进行处理而我们都知道java对象在传输的时候,会进行序列化。
而RedisTemplate底层对这些对象的处理方式就是使用JDK的序列化工具,序列化为字节形式。
但是很遗憾,JDK自带的序列化工具不够“智能”。我们需要更加智能的序列化工具。我们知道在SpringMVC中也有序列化问题,
没错JSON它又来了。
3.3 问题解决
因为实际应用中对于key,基本都以字符串类型进行存储,所以对于key的序列化,我们常常使用StringRedisSerializer这一序列化器即可。
但是对于value的序列化要复杂很多。在实际开发中value的值可能时基本数据类型,可能时java对象等
所以这里可以使用Jackson2JsonRedisSerializer这个序列化器。
我们需要引入
<!-- 引入Jackson依赖-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
当然,如果你是SpringBoot项目,那么SpringBoot自带Jackson序列化器,所以不需要显示引入。
@Configuration
public class RedisConfig {
@Bean(name = "redisTemplate")
public RedisTemplate<String, Object> getRedisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(factory);
//默认的Key序列化器为:JdkSerializationRedisSerializer
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer); // key的序列化类型
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // value的序列化类型
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}
这样再测试一遍
会得到这样的key value
上图中配置的内容就是下图中的属性
默认的序列化器会在aftersetProperties方法里面设置。
四、几个Operations的使用
4.1 ValueOperations
下面是一些set方法
/**
* 对String类型的操作
*/
@Test
public void testString(){
//获取ValueOperations对象
ValueOperations valueOperations = redisTemplate.opsForValue();
//set city ""beijing"
valueOperations.set("city","beijing");
//get city
String city = (String)valueOperations.get("city");//默认返回Object类型
System.out.println(city);
//set student "李明“ ex 10
valueOperations.set("student","李明",10l, TimeUnit.SECONDS);
//setnx city shanghai
Boolean aBoolean = valueOperations.setIfAbsent("city", "shanghai");
System.out.println(aBoolean);
}
4.2 HashOperations
/**
* 操作Hash类型的元素
*/
@Test
public void testHash(){
//HashOperations是操作Hash结构的api集合
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.put("user1","name","xiaoming");
hashOperations.put("user1", "age",10);
Map map = new HashMap();
map.put("name,","lihua");
map.put("age,",12);
hashOperations.putAll("user2",map);
System.out.println((String)hashOperations.get("user1", "name"));
//获取所有的key
Set user1 = hashOperations.keys("user1");
for (Object key : user1) {
System.out.println( key);
}
//获取所有的value
List values = hashOperations.values("user1");
for (Object value : values) {
System.out.println( value);
}
}
4.3 ListOperations
/**
* 操作list类型的元素
*/
@Test
public void testList(){
ListOperations listOperations = redisTemplate.opsForList();
listOperations.leftPush("key", "value1");
listOperations.leftPushAll("key","value2","value3");
List list = listOperations.range("key", 0, -1);
for (Object o : list) {
System.out.println(o);
}
Long size = listOperations.size("key");
int lsize = size.intValue();
for (int i = 0; i < lsize; i++) {
Object value = listOperations.leftPop("key");
System.out.println(value);
}
}
上述只是对redisTemplateAPI列举几个例子
更详细的API的使用可以去看这篇博客
https://blog.csdn.net/zzvar/article/details/118388897?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170486848716800180683954%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=170486848716800180683954&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2blogtop_positive~default-1-118388897-null-null.nonecase&utm_term=redistemplate%E5%B8%B8%E7%94%A8%E6%96%B9%E6%B3%95&spm=1018.2226.3001.4450
五、Redis存储对象的方案
5.1StringRedisTemplate序列化方案
StringRedisTemplate 是专门用来处理字符串的工具类
5.1.1 RedisTemplate序列化的问题
尽管JSON的序列化方式可以满足我们的需求,但依然存在着下图的问题:
当我们向Redis数据库存入一个实体类型的数据后
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String name;
private Integer age;
}
向Redis中存一下数据
@Test
void testSaveUser(){
// 写入数据
redisTemplate.opsForValue().set("user:100",new User("虎哥",21));
// 获取数据
User o = (User) redisTemplate.opsForValue().get("user:100");
System.out.println(o);
}
为了在反序列化时知道对象的类型,JSON序列化器会将类的class类型写入到JSON结果中并存入Redis,会带来额外的内存开销
为了节省内存空间,我们并不会使用JSON序列化器来处理value,而是统一使用String序列化器,要求只能存储String类型的key哈value,当要存储Java对象时,手动完成对象的序列化和反序列化
5.1.2 StringRedisTemplate序列化演示
Spring默认提供了一个StringRedisTemplate类,它的key和value的序列化方式默认就是String方式,省去了我们自定义RedisTemplate的过程(记得注释掉我们自定义的redisTemplate)
之前我们定义的那个配置文件也完全可以删除不要了
下面的程序比RedisTemplate多了一个序列化和反序列化,其他四一个样子的(前提value存储的是对象)
private static final ObjectMapper mapper = new ObjectMapper();
是SpringMVC中默认使用的JSON工具
@Autowired
private StringRedisTemplate stringRedisTemplate;
// JSON工具
private static final ObjectMapper mapper = new ObjectMapper();
@Test
void testStringTemplate() throws JsonProcessingException {
// 准备对象
User user = new User("虎哥", 18);
// 手动序列化
String json = mapper.writeValueAsString(user);
// 写入一条数据到Redis
stringRedisTemplate.opsForValue().set("user:200",json);
// 读取数据
String s = stringRedisTemplate.opsForValue().get("user:200");
// 反序列化
User user1 = mapper.readValue(s,User.class);
System.out.println(user1);
}
5.2 自定义RedisTemplate序列化
实际上。如果我们的value如果想用自定义序列化成的方式进行存储在实际开发中可能会遇到很多问题。其中问题主要在序列化本身上。
仔细想想上面使用StringRedisTemplate本质上和自定义没有区别。实际都是转化为String类型进行存储的,只不过,一个是在程序本身使用ObjectMapper这个工具手动序列化好在给redisTemplate。后者是将对象传入,在RedisTemplate内部通过一系列自定义的配置将其序列化为json字符串存入redis.
下面给一个我开发中常用的自定义RedisTemplate的模板
package com.sky.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.sky.json.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import javax.annotation.Resource;
import java.util.TimeZone;
/**
* RedisConfig
*
* @author baixian
* @date 2024-1-4
*
* 当前配置类不是必须的,因为 Spring Boot 框架会自动装配 RedisTemplate 对象,但是默认的key序列化器为
* JdkSerializationRedisSerializer,导致我们存到Redis中后的数据和原始数据有差别,故设置为
StringRedisSerializer序列化器以及自己定义的序列化器
*/
@Configuration
@Slf4j
public class RedisConfiguration {
@Resource
RedisConnectionFactory redisConnectionFactory;
@Bean("redisTemplate")
public RedisTemplate<Object,Object> redisTemplate(){
log.info("开始创建redis模板对象...");
RedisTemplate<Object,Object> redisTemplate = new RedisTemplate();
//设置redis的连接工厂对象
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置redis key的序列化器
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
//redis 的 value的序列化器
//构造value的序列化器,使用Jackson2JsonRedisSerializer,
// 这个类是 Spring Data Redis 中提供的 Redis 数据序列化和反序列化的工具类,用于将对象序列化为 JSON 字符串。
//Jackson2JsonRedisSerializer 的构造函数中传入了 Object.class,这个参数表示 Jackson 库在进行序列化和反序列化时要考虑所有类型的对象。
// 具体来说,这是因为 Redis 存储的是键值对(key-value),其中值是序列化后的对象。
//传入 Object.class 的作用是告诉 Jackson 库,当从 Redis 中读取值并进行反序列化时,
// 它应该尝试将序列化后的 JSON 数据转换为 Object 类型的 Java 对象。这是为了通用性,因为 Redis 存储的值可能是任何类型的对象。
//然而,这也意味着当你从 Redis 中读取值时,Jackson 会尝试将 JSON 数据转换为通用的 Object 类型。在实际的使用场景中,这可能导致一些问题,因为你可能期望反序列化后的对象是特定的类型,
// 而不是通用的 Object。如果你知道存储在 Redis 中的值的类型,最好是将 Jackson2JsonRedisSerializer 的构造函数参数指定为具体的类型,而不是 Object.class。
// 这样可以更精确地控制反序列化后的对象类型,避免潜在的类型转换问题。
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
// ObjectMapper 是 Jackson 库中的一个关键类,负责执行 JSON 数据与 Java 对象之间的序列化和反序列化。ObjectMapper 的属性配置影响了序列化和反序列化的行为。
ObjectMapper objectMapper = new ObjectMapper();
// JsonAutoDetect.Visibility.ANY 表示所有字段都可见,不管其修饰符是什么(private、protected、public)。这样设置确保了对象的所有字段都会被序列化,不论其修饰符是什么。
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//LaissezFaireSubTypeValidator.instance:这是 Jackson 提供的默认子类型验证器,用于验证类型信息。
//ObjectMapper.DefaultTyping.NON_FINAL:表示默认情况下,只为非 final 类型保留类型信息。这有助于减少序列化后的 JSON 数据的大小。
//JsonTypeInfo.As.PROPERTY:表示类型信息将作为 JSON 属性添加到序列化后的数据中。这意味着 JSON 数据中将包含一个字段,用于存储对象的类型信息。
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
//设置时区信息否则java8中的LocalDateTime在遵循jackson2JsonRedisSerializer序列化的时候就会有问题。
//参考博客:https://blog.csdn.net/xing_hung/article/details/124309758?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170486832516800211595467%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=170486832516800211595467&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~sobaiduend~default-2-124309758-null-null.nonecase&utm_term=%20Could%20not%20write%20JSON%3A%20Java%208%20date%2Ftime%20type%20%60java.time.LocalDateTime%60%20not%20supported%20by%20default%3A%20add%20Module%20com.fasterxml.jackson.datatype%3Ajackson-datatype-jsr310%20to%20enable%20handling%20%28through%20referen&spm=1018.2226.3001.4450
// objectMapper.registerModule(new JavaTimeModule()).setTimeZone(TimeZone.getDefault());
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 表示: value 以 JSON 格式进行序列化。这个序列化器使用了 Jackson 库进行 JSON 序列化。
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
//设置 Hash 结构中的 key 和 value 的序列化器
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
//redisTemplate.afterPropertiesSet() 是在配置 RedisTemplate 完成后进行的一步初始化操作。
// 在 Spring 中,afterPropertiesSet() 方法是 InitializingBean 接口中定义的方法,在 bean 的属性设置完成后,由 Spring 容器自动调用。
//在这里的具体场景中,RedisTemplate 在其属性设置完成后,需要进行一些初始化工作,确保它可以正确地与 Redis 进行交互。
// afterPropertiesSet() 方法是在 Spring 容器完成 bean 的属性注入之后被调用的,用于执行一些初始化逻辑。
//在 RedisTemplate 的源码中,afterPropertiesSet() 方法通常包含了一些必要的配置检查和初始化步骤。
// 例如,它可能会检查连接工厂是否已经设置,序列化器是否已经配置等。这确保了在使用 RedisTemplate 进行操作之前,其内部的配置是正确的。
redisTemplate.afterPropertiesSet();
return redisTemplate;
这里还有几个问题需要说明一下:
1.不知道有没有小伙伴和我一样:对于
@Resource
RedisConnectionFactory redisConnectionFactory;
我一开始使用的是@Autowied注解,或者有的同学使用构造方法
public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
这样去注入会发现idea报红了。
但是实际程序可以跑起来,并且没有业务上的错误,redis也正常存储。
这可能是jenkins的打包机制导致的。
在网上看有人windows 打包可以正常运行,所以猜测是加载机制导致对象还没有加载在spring容器导致无法获取。
六、ObjectMapper说明
ObjectMapper 是 Jackson 库中的一个关键类,负责执行 JSON 数据与 Java 对象之间的序列化和反序列化。ObjectMapper 的属性配置影响了序列化和反序列化的行为。
以下是一些 ObjectMapper 的常见属性和它们的影响:
-
SerializationFeature 和 DeserializationFeature:
这两个枚举类型提供了一系列的特性,用于控制序列化和反序列化的行为。
例如,SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
可以控制是否将 Date 对象序列化为时间戳。 -
Visibility: 通过设置可见性(visibility),可以控制序列化和反序列化时是否包含私有字段、公有字段、Getter 和 Setter 方法。这是通过 setVisibility() 方法进行配置的。
-
DefaultTyping: 决定是否在序列化时包含类型信息。使用 activateDefaultTyping() 方法,可以配置类型信息的保存方式,例如保存到属性中还是作为 JSON 对象的一个字段。
-
DateFormat 和 Locale: 这两个属性用于配置日期的格式和地区,对于日期类型的序列化和反序列化非常重要。
-
AnnotationIntrospector: 通过设置注解检查器,可以控制 Jackson 在进行序列化和反序列化时如何处理类和字段上的注解。这可以通过 setAnnotationIntrospector() 方法进行配置。
-
TypeFactory: 这个属性用于创建 Java 类型的实例,它影响了 Jackson 如何处理泛型类型和参数化类型。
-
JsonNodeFactory: 用于创建 JsonNode 对象,它影响了 Jackson 对 JSON 树结构的处理。
-
JsonInclude: 决定在序列化时是否包括空值以及如何处理默认值。通过 setSerializationInclusion() 方法进行配置。
-
PropertyNamingStrategy: 用于配置字段命名策略,控制序列化和反序列化时字段名的格式。