深入解析Spring Boot中RedisTemplate的序列化机制与JSON存储
在昨天的文章(SpringBoot 响应 json 数据的后端源码处理流程)中,有人留言说,让我唠叨唠叨 SpringBoot 中 redis 的序列化器。
redis 缓存序列化,用的人很多,但是很少有人关注底层的细节,基于此,我就在写一个姊妹篇,聊聊 SpringBoot 中 redis 相关的序列化知识点。
插播一条 AD,阮一峰老师也推荐过的 Hulu AI,即将涨价。
在 SpringBoot 中,RedisTemplate 比较常用。默认使用 JdkSerializationRedisSerializer 来进行序列化。但是默认的序列化会有个问题:Java 端存完了,取 redis-cli 执行key *
,会发现键值都带有\\xAC\\xED\\x00\\x05t\\x00\\x05
这种字符,不方便查看,而且这如果在集群环境下会导致问题!
下面,我们就从 RedisTemplate 聊起吧。
RedisTemplate
可以看到 4 个序列化相关的属性 ,主要是用于KEY
和VALUE
的序列化,比如说我们经常会将 POJO 对象存储到Redis
中,一般情况下会使用JSON
方式序列化成字符串存储到Redis
中 。
SpringBoot 提供的 Redis 数据结构的操作类。
-
ValueOperations 类,提供 Redis String API 操作 -
ListOperations 类,提供 Redis List API 操作 -
SetOperations 类,提供 Redis Set API 操作 -
ZSetOperations 类,提供 Redis ZSet(Sorted Set) API 操作 -
GeoOperations 类,提供 Redis Geo API 操作 -
HyperLogLogOperations 类,提供 Redis HyperLogLog API 操作
StringRedisTemplate
RedisTemplate 支持泛型,StringRedisTemplate K/V 均为 String 类型。
org.springframework.data.redis.core.StringRedisTemplate
继承 RedisTemplate 类,使用 org.springframework.data.redis.serializer.StringRedisSerializer
字符串序列化方式。
RedisSerializer序列化接口
RedisSerializer接口是 Redis 序列化接口,用于 Redis KEY 和 VALUE 的序列化。
RedisSerializer 接口的实现类如下:
默认为 Redis 提供了 11 中的序列化方式,归类一下主要分为:
-
JDK序列化方式(默认) -
String序列化方式 -
JSON序列化方式 -
XML序列化方式
JDK序列化方式(默认)
org.springframework.data.redis.serializer.JdkSerializationRedisSerializer
,默认不配置的情况 RedisTemplate 采用的是该数据序列化方式,可以查看一下源码:
SpringBoot 自动化配置 RedisTemplate Bean 对象时,就未设置默认的序列化方式。绝大多数情况下,并不推荐使用 JdkSerializationRedisSerializer 进行序列化。主要是不方便人工排查数据。我们来做个测试:
运行单元测试:
发现 key 跟 value 的值都是 16 进制字符串,可以看到 key 跟 value 实际上保存的都是以byte[]
字节数组的格式存储:
key 被序列化成这样,线上通过 key 去查询对应的 value 非常不方便,所以 key 肯定是不能被这样序列化的。value 被序列化成这样,除了阅读可能困难一点,不支持跨语言外,实际上也没多大问题。不过,实际线上场景,还是使用 JSON 序列化居多。
String序列化方式
org.springframework.data.redis.serializer.StringRedisSerializer
,字符串和二进制数组都直接转换:
默认的话,StringRedisTemplate的 key 和 value 采用的就是这种序列化方案。
JSON序列话方式
GenericJackson2JsonRedisSerializer
org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer
使用 Jackson 实现 JSON 的序列化方式,Generic 单词翻译过来表示:通用的意思,可以看出,是支持所有类。
RedisConfig配置
通过配置方式选择对应 Redis 数据的序列化方式,配置如下:
package com.example.redis.serializer.config;
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
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 org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
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.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置
*/
@Configuration
public class RedisConfig {
//GenericJackson2JsonRedisSerializer
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
//String的序列化方式
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// 使用GenericJackson2JsonRedisSerializer 替换默认序列化(默认采用的是JDK序列化)
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
//key序列化方式采用String类型
template.setKeySerializer(stringRedisSerializer);
//value序列化方式采用jackson类型
template.setValueSerializer(genericJackson2JsonRedisSerializer);
//hash的key序列化方式也是采用String类型
template.setHashKeySerializer(stringRedisSerializer);
//hash的value也是采用jackson类型
template.setHashValueSerializer(genericJackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
//Jackson2JsonRedisSerializer
//@Bean
//@ConditionalOnMissingBean(name = "redisTemplate")
//public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// RedisTemplate<String, Object> template = new RedisTemplate<>();
// template.setConnectionFactory(factory);
//
// //String的序列化方式
// StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// //使用Jackson2JsonRedisSerialize 替换默认序列化(默认采用的是JDK序列化)
// Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
//
// // 如果采用Jackson2JsonRedisSerializer序列化方式,没有ObjectMapper配置在强转对象的时候会反序列化失败,也就是User user = (User) redisTemplate.opsForValue().get(key) 会失败;
// ObjectMapper objectMapper = new ObjectMapper();
// objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
// jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
//
// //key序列化方式采用String类型
// template.setKeySerializer(stringRedisSerializer);
// //value序列化方式采用jackson类型
// template.setValueSerializer(jackson2JsonRedisSerializer);
// //hash的key序列化方式也是采用String类型
// template.setHashKeySerializer(stringRedisSerializer);
// //hash的value也是采用jackson类型
// template.setHashValueSerializer(jackson2JsonRedisSerializer);
// template.afterPropertiesSet();
// return template;
//}
////FastJsonRedisSerializer
//@Bean("redisTemplate")
//public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
// RedisTemplate<String, Object> template = new RedisTemplate<>();
// template.setConnectionFactory(factory);
//
// //String序列化方式
// StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// // 使用FastJsonRedisSerializer替换默认序列化(默认采用的是JDK序列化)
// FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
//
// //key序列化方式采用String类型
// template.setKeySerializer(stringRedisSerializer);
// //value序列化方式采用jackson类型
// template.setValueSerializer(fastJsonRedisSerializer);
// //hash的key序列化方式也是采用String类型
// template.setHashKeySerializer(stringRedisSerializer);
// //hash的value也是采用jackson类型
// template.setHashValueSerializer(fastJsonRedisSerializer);
// template.afterPropertiesSet();
// return template;
//}
}
运行以下测试类:
@Test
void redisTemplateSerializeTest() {
String redisTemplateStringKey = "redisTemplateStringKey";
String redisTemplateUserObjectKey = "redisTemplateUserObjectKey";
String redisTemplateUserArrayObjectKey = "redisTemplateUserArrayObjectKey";
String redisTemplateJSONObjectKey = "redisTemplateJSONObjectKey";
String redisTemplateJSONArrayKey = "redisTemplateJSONArrayKey";
//序列化String类型和反序列化String类型
redisTemplate.opsForValue().set(redisTemplateStringKey, "austin");
String austin = (String) redisTemplate.opsForValue().get(redisTemplateStringKey);
System.out.println("stringGet: " + austin);
//序列化Object对象类型和反序列化Object对象类型 (User对象)
User user = new User("123", "austin", 25);
redisTemplate.opsForValue().set(redisTemplateUserObjectKey, user);
User userGet = (User) redisTemplate.opsForValue().get(redisTemplateUserObjectKey);
System.out.println("userGet: " + userGet);
//序列化Object对象数组类型和反序列化Object对象数组类型 (User[]对象数组)
User user1 = new User("1", "austin1", 25);
User user2 = new User("2", "austin2", 25);
User[] userArray = new User[]{user1, user2};
redisTemplate.opsForValue().set(redisTemplateUserArrayObjectKey, userArray);
User[] userArrayGet = (User[]) redisTemplate.opsForValue().get(redisTemplateUserArrayObjectKey);
System.out.println("userArrayGet: " + userArrayGet);
//序列化JSONObject对象类型和反序列化JSONObject对象类型
JSONObject jsonObject = new JSONObject();
jsonObject.put("id", "123");
jsonObject.put("name", "austin");
jsonObject.put("age", 25);
redisTemplate.opsForValue().set(redisTemplateJSONObjectKey, jsonObject);
JSONObject jsonObjectGet = (JSONObject) redisTemplate.opsForValue().get(redisTemplateJSONObjectKey);
System.out.println("jsonObjectGet: " + jsonObjectGet);
//序列化JSONArray对象类型和反序列化JSONArray对象类型
JSONArray jsonArray = new JSONArray();
JSONObject jsonObject1 = new JSONObject();
jsonObject1.put("id", "1");
jsonObject1.put("name", "austin1");
jsonObject1.put("age", 25);
JSONObject jsonObject2 = new JSONObject();
jsonObject2.put("id", "1");
jsonObject2.put("name", "austin2");
jsonObject2.put("age", 25);
jsonArray.add(jsonObject1);
jsonArray.add(jsonObject2);
redisTemplate.opsForValue().set(redisTemplateJSONArrayKey, jsonArray);
JSONArray jsonArrayGet = (JSONArray) redisTemplate.opsForValue().get(redisTemplateJSONArrayKey);
System.out.println("jsonArrayGet: " + jsonArrayGet);
}
观察 redis 数据的存储格式:
key- value :
-
字符串类型
Key: redisTemplateStringKey
Value: "austin"
-
对象类型
Key: redisTemplateUserObjectKey
Value:
{
"@class": "com.example.jedisserializefrombytestojson.User",
"id": "123",
"name": "austin",
"age": 25
}
-
对象数组类型
Key: redisTemplateUserArrayObjectKey
Value:
[
"[Lcom.example.jedisserializefrombytestojson.User;",
[
{
"@class": "com.example.jedisserializefrombytestojson.User",
"id": "1",
"name": "austin1",
"age": 25
},
{
"@class": "com.example.jedisserializefrombytestojson.User",
"id": "2",
"name": "austin2",
"age": 25
}
]
]
-
JSONObject类型
Key: redisTemplateJSONObjectKey
Value:
{
"@class": "com.alibaba.fastjson.JSONObject",
"name": "austin",
"id": "123",
"age": 25
}
-
JSONArray类型
Key: redisTemplateJSONArrayKey
Value:
[
"com.alibaba.fastjson.JSONArray",
[
{
"@class": "com.alibaba.fastjson.JSONObject",
"name": "austin1",
"id": "1",
"age": 25
},
{
"@class": "com.alibaba.fastjson.JSONObject",
"name": "austin2",
"id": "1",
"age": 25
}
]
]
运行 redisTemplateSerializeTest 测试类,结果发现该方式序列化和反序列化都没有问题,果然是通用性序列化方式:
我们来思考下,在将一个对象序列化成一个字符串,怎么保证字符串反序列化成对象的类型呢?Jackson 通过 Default Typing,会在字符串多冗余一个类型,这样反序列化就知道具体的类型了。
从结果发现,使用 GenericJackson2JsonRedisSerializer 序列化方式,String 类型、对象、对象数组、JSONObject、JSONArray 序列化和反序列化都没有问题,value 值序列化后多了 @class 属性,反序列化的对象的类型就可以从这里获取到。@class 属性完美解决了反序列化后的对象类型,所以实际项目中,目前很多采用 GenericJackson2JsonRedisSerializer 序列化方式。
Jackson2JsonRedisSerializer
org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer
观察 redis 数据的存储格式:
key- value:
-
字符串类型
Key: redisTemplateStringKey
Value: "austin"
-
对象类型
Key: redisTemplateUserObjectKey
Value:
{
"id": "123",
"name": "austin",
"age": 25
}
与上面 GenericJackson2JsonRedisSerializer 序列化方式结果不同的是,value 没有 @class 属性。
-
对象数组类型
Key: redisTemplateUserArrayObjectKey
Value:
[
{
"id": "1",
"name": "austin1",
"age": 25
},
{
"id": "2",
"name": "austin2",
"age": 25
}
]
与上面 GenericJackson2JsonRedisSerializer 序列化方式结果不同的是,value 没有"@class": "com.example.jedisserializefrombytestojson.User"
对象类型属性。
-
JSONObject类型
Key: redisTemplateJSONObjectKey
Value:
{
"name": "austin",
"id": "123",
"age": 25
}
与上面 GenericJackson2JsonRedisSerializer 序列化方式结果不同的是,value 没有"@class": "com.alibaba.fastjson.JSONObject"
属性。
-
JSONArray类型
Key: redisTemplateJSONArrayKey
Value:
[
{
"name": "austin1",
"id": "1",
"age": 25
},
{
"name": "austin2",
"id": "1",
"age": 25
}
]
与上面 GenericJackson2JsonRedisSerializer 序列化方式结果不同的是,value 没有"com.alibaba.fastjson.JSONArray"
对象类型属性。
Jackson2JsonRedisSerializer 与 GenericJackson2JsonRedisSerializer 序列化结果不同的是,前者并没有 @class 或者 @type 类型属性,这种序列化方式可能会导致获取 redis 数据反序列化成 POJO 对象时候出错,导致反序列化失败,所以一般也很少使用该方式。
比如在对象强制转换的情况,会报错:
报错信息很明显,不能直接将 JSONObject 对象强制转换成 User 对象,不能通过方式获取转换:
//该方式强转会报错
User userGet = (User) redisTemplate.opsForValue().get(redisTemplateUserObjectKey);
而正确的方式应该是:
//通过com.fastxml.jackson的ObjectMapper对象进行转换
Object userObject = redisTemplate.opsForValue().get(redisTemplateUserObjectKey);
ObjectMapper objectMapper = new ObjectMapper();
User userGet = objectMapper.convertValue(userObject, User.class);
System.out.println("userGet: " + userGet);
这也是 redis 序列化和反序列化主要非常注意地方。
总结
采用GenericJackson2JsonRedisSerializer序列化方式对于 String、对象、对象数组、JSONObject、JSONArray 的序列化反序列化操作都正常,对象强转是没有任何问题,但是采用Jackson2JsonRedisSerializer序列化方式在对象强制时,也就是使用 User userGet = (User) redisTemplate.opsForValue().get(redisTemplateUserObjectKey);
方式获取对象,会操作对象转换失败,建议的解决方式是默认都采用 com.fastxml.jackson 的 ObjectMapper 对象进行转换,也就是:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.convertValue(Object fromValue, Class<T> toValueType);
该方式支持将任意类型的 Object 对象转换为相应的实体对象。