Redis序列化与二次Json化

注:以下代码示例以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 对象形式 存进去或读出来的东西,都必须经过序列化与反序列化。

  1. 所有数据结构在 Redis 层 → 都是 byte[]
  2. 选择 opsForXXX 是为了匹配 Redis 不同结构,不是为了减少序列化
  3. 序列化器是针对(key/value/hashKey/hashValue)不同位置配置的
  4. 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

相关推荐
星辰_mya3 小时前
Redis持久化
数据库·redis·缓存
橘子真甜~3 小时前
Reids命令原理与应用1 - Redis命令与原理
数据库·c++·redis·缓存
与遨游于天地3 小时前
了解Redis
数据库·redis·缓存
陌路204 小时前
redis的哨兵模式
数据库·redis·缓存
CodeAmaz5 小时前
Redis与数据库双写一致性详解
数据库·redis·缓存·数据一致性
学习编程的Kitty5 小时前
Redis(2)——事务
数据库·redis·缓存
JosieBook5 小时前
【大模型】用 AI Ping 免费体验 GLM-4.7 与 MiniMax M2.1:从配置到实战的完整教程
数据库·人工智能·redis
shuair5 小时前
redis缓存双写
redis·缓存·mybatis