深入解析Spring Boot中RedisTemplate的序列化機(jī)制與JSON存儲(chǔ)
在昨天的文章(SpringBoot 響應(yīng) json 數(shù)據(jù)的后端源碼處理流程)中,有人留言說(shuō),讓我嘮叨嘮叨 SpringBoot 中 redis 的序列化器。
redis 緩存序列化,用的人很多,但是很少有人關(guān)注底層的細(xì)節(jié),基于此,我就在寫一個(gè)姊妹篇,聊聊 SpringBoot 中 redis 相關(guān)的序列化知識(shí)點(diǎn)。
插播一條 AD,阮一峰老師也推薦過(guò)的 Hulu AI,即將漲價(jià)。
在 SpringBoot 中,RedisTemplate 比較常用。默認(rèn)使用 JdkSerializationRedisSerializer 來(lái)進(jìn)行序列化。但是默認(rèn)的序列化會(huì)有個(gè)問(wèn)題:Java 端存完了,取 redis-cli 執(zhí)行key *,會(huì)發(fā)現(xiàn)鍵值都帶有\\xAC\\xED\\x00\\x05t\\x00\\x05這種字符,不方便查看,而且這如果在集群環(huán)境下會(huì)導(dǎo)致問(wèn)題!
下面,我們就從 RedisTemplate 聊起吧。
RedisTemplate
可以看到 4 個(gè)序列化相關(guān)的屬性 ,主要是用于KEY和VALUE的序列化,比如說(shuō)我們經(jīng)常會(huì)將 POJO 對(duì)象存儲(chǔ)到Redis中,一般情況下會(huì)使用JSON方式序列化成字符串存儲(chǔ)到Redis中 。
SpringBoot 提供的 Redis 數(shù)據(jù)結(jié)構(gòu)的操作類。
-
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 接口的實(shí)現(xiàn)類如下:
默認(rèn)為 Redis 提供了 11 中的序列化方式,歸類一下主要分為:
-
JDK序列化方式(默認(rèn)) -
String序列化方式 -
JSON序列化方式 -
XML序列化方式
JDK序列化方式(默認(rèn))
org.springframework.data.redis.serializer.JdkSerializationRedisSerializer,默認(rèn)不配置的情況 RedisTemplate 采用的是該數(shù)據(jù)序列化方式,可以查看一下源碼:
SpringBoot 自動(dòng)化配置 RedisTemplate Bean 對(duì)象時(shí),就未設(shè)置默認(rèn)的序列化方式。絕大多數(shù)情況下,并不推薦使用 JdkSerializationRedisSerializer 進(jìn)行序列化。主要是不方便人工排查數(shù)據(jù)。我們來(lái)做個(gè)測(cè)試:
運(yùn)行單元測(cè)試:
發(fā)現(xiàn) key 跟 value 的值都是 16 進(jìn)制字符串,可以看到 key 跟 value 實(shí)際上保存的都是以byte[]字節(jié)數(shù)組的格式存儲(chǔ):
key 被序列化成這樣,線上通過(guò) key 去查詢對(duì)應(yīng)的 value 非常不方便,所以 key 肯定是不能被這樣序列化的。value 被序列化成這樣,除了閱讀可能困難一點(diǎn),不支持跨語(yǔ)言外,實(shí)際上也沒多大問(wèn)題。不過(guò),實(shí)際線上場(chǎng)景,還是使用 JSON 序列化居多。
String序列化方式
org.springframework.data.redis.serializer.StringRedisSerializer,字符串和二進(jìn)制數(shù)組都直接轉(zhuǎn)換:
默認(rèn)的話,StringRedisTemplate的 key 和 value 采用的就是這種序列化方案。
JSON序列話方式
GenericJackson2JsonRedisSerializer
org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer 使用 Jackson 實(shí)現(xiàn) JSON 的序列化方式,Generic 單詞翻譯過(guò)來(lái)表示:通用的意思,可以看出,是支持所有類。
RedisConfig配置
通過(guò)配置方式選擇對(duì)應(yīng) Redis 數(shù)據(jù)的序列化方式,配置如下:
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 替換默認(rèn)序列化(默認(rèn)采用的是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 替換默認(rèn)序列化(默認(rèn)采用的是JDK序列化)
// Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
//
// // 如果采用Jackson2JsonRedisSerializer序列化方式,沒有ObjectMapper配置在強(qiáng)轉(zhuǎn)對(duì)象的時(shí)候會(huì)反序列化失敗,也就是User user = (User) redisTemplate.opsForValue().get(key) 會(huì)失敗;
// 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替換默認(rèn)序列化(默認(rèn)采用的是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;
//}
}
運(yùn)行以下測(cè)試類:
@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對(duì)象類型和反序列化Object對(duì)象類型 (User對(duì)象)
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對(duì)象數(shù)組類型和反序列化Object對(duì)象數(shù)組類型 (User[]對(duì)象數(shù)組)
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對(duì)象類型和反序列化JSONObject對(duì)象類型
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對(duì)象類型和反序列化JSONArray對(duì)象類型
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 數(shù)據(jù)的存儲(chǔ)格式:
key- value :
-
字符串類型
Key: redisTemplateStringKey
Value: "austin"
-
對(duì)象類型
Key: redisTemplateUserObjectKey
Value:
{
"@class": "com.example.jedisserializefrombytestojson.User",
"id": "123",
"name": "austin",
"age": 25
}
-
對(duì)象數(shù)組類型
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
}
]
]
運(yùn)行 redisTemplateSerializeTest 測(cè)試類,結(jié)果發(fā)現(xiàn)該方式序列化和反序列化都沒有問(wèn)題,果然是通用性序列化方式:
我們來(lái)思考下,在將一個(gè)對(duì)象序列化成一個(gè)字符串,怎么保證字符串反序列化成對(duì)象的類型呢?Jackson 通過(guò) Default Typing,會(huì)在字符串多冗余一個(gè)類型,這樣反序列化就知道具體的類型了。
從結(jié)果發(fā)現(xiàn),使用 GenericJackson2JsonRedisSerializer 序列化方式,String 類型、對(duì)象、對(duì)象數(shù)組、JSONObject、JSONArray 序列化和反序列化都沒有問(wèn)題,value 值序列化后多了 @class 屬性,反序列化的對(duì)象的類型就可以從這里獲取到。@class 屬性完美解決了反序列化后的對(duì)象類型,所以實(shí)際項(xiàng)目中,目前很多采用 GenericJackson2JsonRedisSerializer 序列化方式。
Jackson2JsonRedisSerializer
org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer
觀察 redis 數(shù)據(jù)的存儲(chǔ)格式:
key- value:
-
字符串類型
Key: redisTemplateStringKey
Value: "austin"
-
對(duì)象類型
Key: redisTemplateUserObjectKey
Value:
{
"id": "123",
"name": "austin",
"age": 25
}
與上面 GenericJackson2JsonRedisSerializer 序列化方式結(jié)果不同的是,value 沒有 @class 屬性。
-
對(duì)象數(shù)組類型
Key: redisTemplateUserArrayObjectKey
Value:
[
{
"id": "1",
"name": "austin1",
"age": 25
},
{
"id": "2",
"name": "austin2",
"age": 25
}
]
與上面 GenericJackson2JsonRedisSerializer 序列化方式結(jié)果不同的是,value 沒有"@class": "com.example.jedisserializefrombytestojson.User"對(duì)象類型屬性。
-
JSONObject類型
Key: redisTemplateJSONObjectKey
Value:
{
"name": "austin",
"id": "123",
"age": 25
}
與上面 GenericJackson2JsonRedisSerializer 序列化方式結(jié)果不同的是,value 沒有"@class": "com.alibaba.fastjson.JSONObject"屬性。
-
JSONArray類型
Key: redisTemplateJSONArrayKey
Value:
[
{
"name": "austin1",
"id": "1",
"age": 25
},
{
"name": "austin2",
"id": "1",
"age": 25
}
]
與上面 GenericJackson2JsonRedisSerializer 序列化方式結(jié)果不同的是,value 沒有"com.alibaba.fastjson.JSONArray"對(duì)象類型屬性。
Jackson2JsonRedisSerializer 與 GenericJackson2JsonRedisSerializer 序列化結(jié)果不同的是,前者并沒有 @class 或者 @type 類型屬性,這種序列化方式可能會(huì)導(dǎo)致獲取 redis 數(shù)據(jù)反序列化成 POJO 對(duì)象時(shí)候出錯(cuò),導(dǎo)致反序列化失敗,所以一般也很少使用該方式。
比如在對(duì)象強(qiáng)制轉(zhuǎn)換的情況,會(huì)報(bào)錯(cuò):
報(bào)錯(cuò)信息很明顯,不能直接將 JSONObject 對(duì)象強(qiáng)制轉(zhuǎn)換成 User 對(duì)象,不能通過(guò)方式獲取轉(zhuǎn)換:
//該方式強(qiáng)轉(zhuǎn)會(huì)報(bào)錯(cuò)
User userGet = (User) redisTemplate.opsForValue().get(redisTemplateUserObjectKey);
而正確的方式應(yīng)該是:
//通過(guò)com.fastxml.jackson的ObjectMapper對(duì)象進(jìn)行轉(zhuǎn)換
Object userObject = redisTemplate.opsForValue().get(redisTemplateUserObjectKey);
ObjectMapper objectMapper = new ObjectMapper();
User userGet = objectMapper.convertValue(userObject, User.class);
System.out.println("userGet: " + userGet);
這也是 redis 序列化和反序列化主要非常注意地方。
總結(jié)
采用GenericJackson2JsonRedisSerializer序列化方式對(duì)于 String、對(duì)象、對(duì)象數(shù)組、JSONObject、JSONArray 的序列化反序列化操作都正常,對(duì)象強(qiáng)轉(zhuǎn)是沒有任何問(wèn)題,但是采用Jackson2JsonRedisSerializer序列化方式在對(duì)象強(qiáng)制時(shí),也就是使用 User userGet = (User) redisTemplate.opsForValue().get(redisTemplateUserObjectKey);方式獲取對(duì)象,會(huì)操作對(duì)象轉(zhuǎn)換失敗,建議的解決方式是默認(rèn)都采用 com.fastxml.jackson 的 ObjectMapper 對(duì)象進(jìn)行轉(zhuǎn)換,也就是:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.convertValue(Object fromValue, Class<T> toValueType);
該方式支持將任意類型的 Object 對(duì)象轉(zhuǎn)換為相應(yīng)的實(shí)體對(duì)象。
