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

相关推荐
老男孩游戏时光3 小时前
# Redis分布式锁三种实现方案,踩坑记录
redis
石小千3 小时前
Redis开启ACL认证
数据库·redis·缓存
hweiyu005 小时前
Redis命令:LPOS
redis·缓存
guo_wen_qiang5 小时前
云服务器redis主从复制-哨兵模式
运维·服务器·redis
for_ever_love__5 小时前
LangGraph State 设计:多节点之间怎么安全地共享数据
redis·mysql·状态模式·checkpoint·state·状态图·langgraph
大圣编蚕6 小时前
Redis 为什么快?现在还是单线程吗?
数据库·redis·缓存
Linux实战笔记6 小时前
记一次Redis分布式锁实现线上排查:Redlock算法与 Lua 脚本
redis·高并发·分布式锁·redlock 算法·lua 脚本
轮子大叔7 小时前
Redis深入浅出
redis·缓存
计算机就业还在扩大7 小时前
Redis中的setnx是原子性的?
数据库·redis·缓存
Java爱好狂.10 小时前
Java小白如何三天速通redis?
java·数据库·redis·架构师·分布式锁·java面试·java程序员