注:以下代码示例以Google的Gson框架为例 其他框架如FastJson/Jackson可自行了解
1. Redis 中的数据本质
Redis 本身并不理解对象、JSON 或类型语义。 所谓"在 Redis 里存 JSON",只是应用层的约定,不是 Redis 的能力。 它只做一件事:存储字节数组(byte[])。
vbnet
key -> byte[] value -> byte[]
在 Java 中,对象无法直接写入 Redis,必须经历:
css
Java Object → 序列化(serialize) // toJson → byte[] //getbytes() → Redis
读取时则是反向过程:
css
byte[] → 反序列化(deserialize) → Java Object
因此,序列化策略是 Java 客户端的职责,而不是 Redis 的职责。
问题几乎都出在:写入时用的序列化方式 ≠ 读取时用的反序列化方式。
2. 常见序列化方式对比
序列化方式对比表
| 序列化方式 | 存储内容 | 可读性 | 兼容性 | 常见问题 |
| JDK 原生序列化 | 二进制 | 差 | 极差(强依赖类结构) | 类变更直接反序列化失败 |
| JSON 序列化 | JSON 字节 | 好 | 好 | 易出现二次 JSON 化 |
| String 序列化 | 字符串字节 | 最好 | 取决于约定 | 需要自行维护类型 |
JSON 序列化是最常见也是最容易踩坑的一种,原因不是 JSON 本身,而是职责混乱。
Jackson 的 Redis 序列化配置示例
下面配置只是用于说明JSON 序列化在 Redis 中是如何接管 byte[] 的,后文代码示例仍然使用 Gson。
java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// Key 使用 String 序列化(保持可读性)
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// Value 使用 JSON 序列化(避免 JDK 序列化)
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
这个配置的关键点只有一句话: RedisTemplate 已经负责"对象 ↔ JSON ↔ byte[]"的全部过程 如果你在写入前再手动 toJsonString,问题就埋下了。
3. 二次 JSON 化的典型成因
什么是二次 JSON 化
二次 JSON 化指的是:
对象 → JSON 字符串(第一次) → 再次 JSON 序列化(第二次) → Redis
最终 Redis 中存的不是对象的 JSON,而是JSON 字符串本身的 JSON 表示。
典型错误写法(Gson 示例)
java
Gson gson = new Gson();
User user = new User(1L, "Alice"); // 第一次 JSON 化(手动)
String json = gson.toJson(user); // 第二次 JSON 化(Redis 序列化器) redisTemplate.opsForValue().set("user:1", json);
如果 RedisTemplate 的 value 序列化器是 JSON(Jackson 或 Gson), 那么 json 会被当作一个普通 String 再序列化一次。
Redis 中的实际存储结果
期望存的是:
java
{"id":1,"name":"Alice"} //Object
100 //Integer
true //Boolean
hello //String 但是你在java中得String s = "hello"
//这里不要混淆 一般来讲,正常存储到redis的数据一般是不会带两侧的""的
实际存的是:
java
"{"id":1,"name":"Alice"}" //String
"100" //String
"true" //String
"hello" //String
特征非常明显:
- 最外层多了一层引号
- 内部充满转义字符
- 语义已经从「对象」变成了「字符串」
另一种高频成因:模板混用
java
StringRedisTemplate.opsForValue().set(key, gson.toJson(obj)); // 写入
redisTemplate.opsForValue().get(key); // 读取
StringRedisTemplate 默认使用 String 序列化
RedisTemplate 期望 JSON 反序列化
写和读的"语言体系"完全不同
4.代码案例
java
void test_toJson() {
RegionDo region = buildRegion();
String json = gson.toJson(region);
System.out.println(json);
String json1 = gson.toJson(json);
System.out.println(json1);
}
把对象直接set给redisTemplate,那么存的就是标准json格式,如果手动toJson后再丢给redisTemplate,那么存的就是后者
那么如何正常解析呢?
第一步:
java
JsonElement jsonElement = JsonParser.parseString(s);
JsonElement只有四个实现类,我们分开讲

经过一次parseString,标准流程存储的数据都可以被成功反序列化
第二步:
对于以上所说的所有被二次Json化的数据,全部属于JsonPrimitive中的String
java
void test() {
String s1 = ""{\"name\":\"Alice\",\"age\":25}""; //二次Json的User对象
String s2 = ""[{\"name\":\"Alice\",\"id\":25},{\"name\":\"Bob\",\"id\":30}]""; //二次Json的List<User>
String s3 = ""\100""; //二次Json的Integer
String s4 = ""true""; //二次Json的Boolean
System.out.println(JsonParser.parseString(s1).getClass());
System.out.println(JsonParser.parseString(s2).getClass());
System.out.println(JsonParser.parseString(s3).getClass());
System.out.println(JsonParser.parseString(s4).getClass());
}
class com.google.gson.JsonPrimitive
class com.google.gson.JsonPrimitive
class com.google.gson.JsonPrimitive
class com.google.gson.JsonPrimitive
//另外强调下,这里之所以这么多\ 是为了转义,在java中手动new string模拟redis中的二次Json化的数据
那么这里就清晰了,我们可以
java
String asString = jsonElement.getAsString();
return gson.fromJson(asString,clazz);
小结
二次 JSON 化从来不是"序列化器的 Bug",而是:
应用层重复承担了本应由 RedisTemplate 负责的序列化职责
一旦对象被提前 toJson,后续所有 JSON 序列化器都会"再补一刀"。
Tips:
1.string经过 serialize()方法只需要getBytes(),这不属于序列化,所以有的帖子会说string不需要序列化
2.hash结构的数据如果getall,那么序列化或者反序列化都需要经过hash.size()次,也就是每个entry都要重复此步骤
3.有时候我们使用Object接收返回的对象,实际上在底层的deserialize()方法中有:return gson.fromJson(asString,clazz); 这里拿到的已经是User了,只是你用Object接收就得强转通过编译器检查,才能调用user自己的get方法
java
RedisTemplate\<String, Object> redisTemplate;
Object obj = redisTemplate.opsForValue().get("user");//obj.getClass()是User
User user = (User) obj; // ✅ 必须强转是因为编译器不知道,jvm内存层面其实是User
user.getName();
- 反序列化时已经 new 出了 User
- 但编译器只知道你拿到的是 Object
- Java 是编译期静态类型语言
👉 所以强转是给编译器看的,不是给 JVM 的
4.各 Ops 使用的序列化器组合
| Redis 操作类型 | key 序列化 | value 序列化 | hash key 序列化 | hash value 序列化 |
| opsForValue() | keySerializer | valueSerializer | ❌ | ❌ |
| opsForHash() | keySerializer | ❌ | hashKeySerializer | hashValueSerializer |
| opsForList() | keySerializer | valueSerializer | ❌ | ❌ |
| opsForSet() | keySerializer | valueSerializer | ❌ | ❌ |
| opsForZSet() | keySerializer | valueSerializer | ❌ | ❌ |
核心总结:
- 普通 key-value → key/value 序列化器
- hash → hashKey/hashValue 序列化器
- 没有自己的序列化器,就回退去 valueSerializer 或 keySerializer
Redis 是一个纯二进制存储系统。
凡是通过 RedisTemplate 以 Java 对象形式 存进去或读出来的东西,都必须经过序列化与反序列化。
- 所有数据结构在 Redis 层 → 都是 byte[]
- 选择 opsForXXX 是为了匹配 Redis 不同结构,不是为了减少序列化
- 序列化器是针对(key/value/hashKey/hashValue)不同位置配置的
- opsForXXX 选择序列化器的逻辑是结构驱动
5.@class字段
如果你redis中的 JSON 包含 @class 字段(由 GenericJackson2JsonRedisSerializer 生成):
less
{ "@class": "com.example.User", "id": 1, "name": "Alice" }
那么你可以:
dart
Object obj = mapper.readValue(jsonWithClass, Object.class); // ✅ 自动还原为 User!
✅ 这是 Jackson 在 Redis / Spring 场景下的巨大优势:一个 RedisTemplate 能存/取任意类
但同时存在RCE漏洞。FastJson曾经因此出现重大bug,生产一般不开启此功能。
6.明确一点,new String最外侧的""只是标识作用,并不参与到字符串的内容中
String s = "hello" // s.length()=5
String s = ""hello"" //真实内容是"hello",可以理解为这就是被二次Json化的数据,s.length() = 7