Redis 数据存储与序列化完全指南:从 byte[] 到 Object:Redis 序列化机制与生产环境选型

从底层 SDS 到上层序列化器,一篇讲透 Redis 到底存了什么、怎么存的、该选哪个序列化方案。


一、Redis 底层到底存了什么?

1.1 一句话结论

不管你在 Java 里存的是 String、Object、int 还是 byte\[\],到了 Redis 底层,全是 byte\[\]。

1.2 最底层:SDS(Simple Dynamic String)

Redis 是 C 语言编写的,没有 使用 C 语言自带的 char*,而是自己实现了 SDS 结构体:

c 复制代码
// Redis 源码:src/sds.h

struct sdshdr {
    int len;        // 已使用的字节数
    int free;       // 剩余可用空间
    char buf[];     // 实际数据(就是 byte[])
};

"张三"(UTF-8 编码,6 字节):

复制代码
内存地址 0x7f001000:

┌─────────┬──────────┬──────────────────────────────────────────┐
│ len = 6 │ free = 2 │ buf: [E5][BC][A0][E4][B8][89][\0][\0]   │
│ (4字节) │ (4字节)  │      ← 6字节有效数据 →  ← 2字节预留 →    │
└─────────┴──────────┴──────────────────────────────────────────┘

存 gzip 二进制 [1F 8B 08 00 CB 48 ...]

复制代码
内存地址 0x7f002000:

┌──────────┬──────────┬─────────────────────────────────────────────────┐
│ len = 31 │ free = 1 │ buf: [1F][8B][08][00][00][00][CB][48]...[00]   │
│ (4字节)  │ (4字节)  │      ← 31字节,原封不动 →                  [00] │
└──────────┴──────────┴─────────────────────────────────────────────────┘

不管存什么,底层就是一块连续的 char buf[](字节数组),前面加一个 int len 记录长度。没有类型信息,没有编码信息,就是裸的 byte\[\]。

1.3 为什么不用 C 的 char*

c 复制代码
// C 语言原生字符串:
char *s = "hello";
// 内存:[68][65][6C][6C][6F][00]
//                          ↑ 靠 0x00 判断结尾
// 问题:数据中间有 0x00 就会被截断!

// Redis 的 SDS:
// 内存:[len=5][free=0][68][65][6C][6C][6F]
//        ↑ 靠 len 字段知道长度,不靠 0x00
// 所以中间有 0x00 也无所谓

1.4 上层封装:redisObject

每个 key-value 在 Redis 中是一个 redisObject

c 复制代码
// Redis 源码:src/server.h

typedef struct redisObject {
    unsigned type:4;      // 类型:STRING / HASH / LIST / SET / ZSET
    unsigned encoding:4;  // 编码方式:RAW / INT / EMBSTR
    void *ptr;            // 指向实际数据的指针(指向 SDS)
} robj;

完整内存结构:

复制代码
key "mydata":
┌────────────────────────────┐
│ redisObject                │
│   type = STRING (0)        │
│   encoding = RAW (1)       │
│   ptr ──────────────────┐  │
└─────────────────────────┼──┘
                          │
                          ▼
              ┌─────────────────────────────────────┐
              │ SDS                                  │
              │   len = 31                           │
              │   free = 1                           │
              │   buf = [1F 8B 08 00 00 ... 00 00]  │
              └─────────────────────────────────────┘

1.5 String 类型的三种编码方式

条件 encoding 存储方式
值是整数且 ≤ long 范围 INT 直接把整数存在 ptr 里(不分配 SDS)
字节数 ≤ 44 EMBSTR redisObject 和 SDS 连在一起,一次 malloc
字节数 > 44 RAW redisObject 和 SDS 分开,两次 malloc
复制代码
INT(存数字 25):
┌──────────────────┐
│ redisObject      │
│   type = STRING  │
│   encoding = INT │
│   ptr = 25       │  ← 直接存数字,没有 SDS
└──────────────────┘

EMBSTR(存 "hello",5字节 ≤ 44):
┌──────────────────────────────────────────┐
│ redisObject │ SDS (len=5, buf="hello")   │  ← 一块连续内存
└──────────────────────────────────────────┘

RAW(存一个 100 字节的图片数据):
┌─────────────┐     ┌────────────────────────────────┐
│ redisObject │ ──→ │ SDS (len=100, buf=[...])       │  ← 两块内存
└─────────────┘     └────────────────────────────────┘

二、读写 Redis 时使用的类型

2.1 核心结论

  • 客户端 API 层面 :你传 Stringbyte[]
  • 网络传输层面 :全是 byte[]
  • Redis 存储层面 :全是 byte[]

其他类型(int、Object)都是你的代码客户端库帮你转换的,Redis 本身不认识也不关心。

2.2 各语言客户端对比

Java(Jedis):

java 复制代码
// Jedis 的 set 方法只有两个重载:
// String set(String key, String value);
// String set(byte[] key, byte[] value);

// 写入
jedis.set("name", "张三");
jedis.set("mydata".getBytes(), gzipBytes);

// 读取
String name = jedis.get("name");
byte[] data = jedis.get("mydata".getBytes());

Python(redis-py):

python 复制代码
# 写入:可以传 str / bytes / int / float
r.set("name", "张三")          # str → 内部自动 .encode("utf-8")
r.set("age", 25)              # int → 内部自动 str(25).encode()
r.set("data", b'\x1f\x8b')   # bytes → 直接用

# 读取:默认返回 bytes
data = r.get("name")          # → b'\xe5\xbc\xa0\xe4\xb8\x89'

# 想要 str?设置 decode_responses=True
r = redis.Redis(decode_responses=True)
data = r.get("name")          # → "张三"

Go(go-redis):

go 复制代码
// 写入:value 是 interface{}
r.Set(ctx, "name", "张三", 0)
r.Set(ctx, "data", []byte{0x1f, 0x8b}, 0)

// 读取:你自己选要什么类型
val, _ := r.Get(ctx, "name").Result()    // string
val, _ := r.Get(ctx, "data").Bytes()     // []byte
num, _ := r.Get(ctx, "age").Int()        // int

2.3 完整数据流转链路

复制代码
你的代码              客户端库                网络传输            Redis
──────              ──────                ──────            ──────

"张三"(String)  →  .getBytes("UTF-8")  →  [E5 BC A0...]  →  存 byte[]
                   转成 byte[]            TCP 字节流

25 (int)        →  "25".getBytes()     →  [32 35]        →  存 byte[]
                   先转String再转byte[]

gzip(byte[])    →  不用转,直接用       →  [1F 8B 08...]  →  存 byte[]

读取时反过来:

Redis 返回 byte[] → 客户端收到 byte[] →  转成你要的类型
                                         .Result() → String
                                         .Bytes()  → byte[]
                                         .Int()    → int

2.4 各场景类型总结

场景 写入时用什么类型 读取时拿到什么类型
存文字 String String
存二进制(gzip/图片) byte[] byte[]
存数字 String("25")或 int(客户端帮转) String("25"),自己转 int
存对象 自己 序列化成 byte[] 或 JSON String 自己反序列化

三、Spring RedisTemplate 序列化机制

3.1 RedisTemplate 不是只支持 String 和 byte\[\]

RedisTemplate 通过序列化器(Serializer) 机制,可以直接存取 Object:

java 复制代码
// RedisTemplate 有 4 个序列化器可以配置:
redisTemplate.setKeySerializer(...)       // key 怎么序列化
redisTemplate.setValueSerializer(...)     // value 怎么序列化
redisTemplate.setHashKeySerializer(...)   // hash 的 field 怎么序列化
redisTemplate.setHashValueSerializer(...) // hash 的 value 怎么序列化

3.2 Spring 提供的序列化器

序列化器 存进去是什么 支持类型
StringRedisSerializer 纯文本 "张三" String
JdkSerializationRedisSerializer默认 Java 二进制 \xAC\xED\x00\x05... 任何实现了 Serializable 的 Object
Jackson2JsonRedisSerializer JSON 文本 {"name":"张三","age":25} 任何 Object
GenericJackson2JsonRedisSerializer JSON 文本(带 @class 类型信息) 任何 Object

3.3 默认行为:直接存 Object

java 复制代码
@Autowired
private RedisTemplate<Object, Object> redisTemplate;

// 直接存对象!不用手动转!
User user = new User("张三", 25);
redisTemplate.opsForValue().set("user:1", user);  // ✅

// 直接读对象!
User user = (User) redisTemplate.opsForValue().get("user:1");  // ✅

⚠️ 注意:User 类必须实现 Serializable 接口!

java 复制代码
public class User implements Serializable {  // ← 必须!
    private String name;
    private int age;
}

3.4 使用 JSON 序列化器

java 复制代码
@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // key 用 String
        template.setKeySerializer(new StringRedisSerializer());

        // value 用 JSON
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        return template;
    }
}
java 复制代码
// 使用:还是直接存对象!
User user = new User("张三", 25);
redisTemplate.opsForValue().set("user:1", user);  // ✅

// Redis 里实际存的是:
// {"@class":"com.xxx.User","name":"张三","age":25}

3.5 使用 StringRedisTemplate

java 复制代码
@Autowired
private StringRedisTemplate stringRedisTemplate;

// 只能存 String!
stringRedisTemplate.opsForValue().set("name", "张三");  // ✅
stringRedisTemplate.opsForValue().set("user", user);     // ❌ 编译报错!

// 想存对象?自己转 JSON:
String json = JSON.toJSONString(user);
stringRedisTemplate.opsForValue().set("user:1", json);   // ✅

// 读出来自己转:
String json = stringRedisTemplate.opsForValue().get("user:1");
User user = JSON.parseObject(json, User.class);

3.6 Jackson 序列化的完整链路

复制代码
Object
  │
  ▼  Jackson2JsonRedisSerializer
JSON String: {"name":"张三","age":25}
  │
  ▼  .getBytes("UTF-8")
byte[]: [7B 22 6E 61 6D 65 22 3A 22 E5 BC A0 ...]
  │
  ▼  TCP 网络传输
Redis 存 byte[]

先转 JSON 字符串,再转 byte\[\],两步。


四、常用序列化器全面对比

4.1 总览对比表

对比项 StringRedisSerializer JdkSerializationRedisSerializer Jackson2JsonRedisSerializer GenericJackson2JsonRedisSerializer
输入类型 只能 String 任何 Object(需 Serializable) 任何 Object 任何 Object
Redis 里存的 纯文本 "张三" \xAC\xED\x00\x05sr\x00... {"name":"张三","age":25} {"@class":"com.xx.User","name":"张三"}
可读性 ✅ 完全可读 ❌ 二进制乱码 ✅ JSON 可读 ✅ JSON 可读
跨语言 ❌ 只有 Java ⚠️ @class 是 Java 概念
需要 Serializable ✅ 必须
体积 最小 最大(带元信息) 中(多一个 @class 字段)
性能 极快(UTF-8 编解码) 慢(反射 + 元信息) 中(反射) 中(反射)
版本兼容 ❌ 改字段名就炸 ⚠️ 改包名就炸
安全性 ⚠️ 反序列化漏洞
调试 ✅ redis-cli 直接看 ❌ 乱码
一个 template 存多种类型 ✅ 天然支持 ❌ 绑定一个 class ✅ 自动识别
读取时类型还原 你自己指定 自动 ⚠️ 用 Object.class 读出 LinkedHashMap 自动(靠 @class)
额外依赖 无(JDK 自带) Jackson Jackson

4.2 各序列化器存储内容对比

同一个对象 User("张三", 25),各序列化器存进 Redis 的内容:

复制代码
StringRedisSerializer(你手动转 JSON 后):
{"name":"张三","age":25}

JdkSerializationRedisSerializer:
\xAC\xED\x00\x05\x73\x72\x00\x1A\x63\x6F\x6D\x2E\x65\x78\x61\x6D\x70\x6C\x65\x2E\x55\x73\x65\x72...
(完全不可读的二进制,包含类名、字段名、字段类型、serialVersionUID 等元信息)

Jackson2JsonRedisSerializer:
{"name":"张三","age":25}

GenericJackson2JsonRedisSerializer:
{"@class":"com.example.User","name":"张三","age":25}

4.3 各序列化器详细分析

① StringRedisSerializer
java 复制代码
// 原理:极其简单
public byte[] serialize(String string) {
    return string.getBytes(charset);  // 就这一行
}
public String deserialize(byte[] bytes) {
    return new String(bytes, charset);
}

优点:

  • 性能最好,零反射开销
  • 完全透明,存什么就是什么
  • 一个 template 存任意类型(你自己转)
  • 跨语言完美兼容

缺点:

  • 只能处理 String,存对象要自己转
  • 多写两行代码(toJSONString / parseObject)

适用场景: 缓存验证码、token、分布式锁、计数器、配置项、高 QPS 缓存


② JdkSerializationRedisSerializer(RedisTemplate 默认)
java 复制代码
// 原理:Java 原生序列化
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(user);  // 反射所有字段 + 写入类元信息

优点:

  • 不需要额外依赖
  • 自动处理对象图(循环引用、继承)

缺点:

  • 必须实现 Serializable
  • 体积大(带类名、字段描述、serialVersionUID)
  • 完全不可读,无法调试
  • 改个字段名/包名,旧缓存全部反序列化失败
  • 有反序列化安全漏洞(RCE)
  • 只有 Java 能读
  • 性能差

适用场景: 基本不推荐。唯一的"优点"是零配置,但代价太大。


③ Jackson2JsonRedisSerializer
java 复制代码
// 创建时必须指定类型
Jackson2JsonRedisSerializer<User> serializer = 
    new Jackson2JsonRedisSerializer<>(User.class);

优点:

  • JSON 可读
  • 不需要 Serializable
  • 跨语言
  • 没有 @class,不存在包名耦合

缺点:

  • 一个 serializer 绑定一个类型
  • Object.class 创建 → 读出来是 LinkedHashMap,不是原对象
  • 一个 RedisTemplate 只能配一个 valueSerializer → 只能存一种类型

适用场景: 整个项目只缓存一种类型的对象(少见)


④ GenericJackson2JsonRedisSerializer
java 复制代码
// 不需要指定类型,自动处理
GenericJackson2JsonRedisSerializer serializer = 
    new GenericJackson2JsonRedisSerializer();

优点:

  • JSON 可读
  • 自动写入 @class,读取时自动还原正确类型
  • 一个 template 存多种类型
  • 不需要 Serializable

缺点:

  • @class 写入 Redis → 改包名/类名后旧缓存全部报错
  • 跨语言时 @class 字段无意义
  • 体积比 Jackson2Json 稍大

适用场景: 纯 Java 项目、对象类型多、开发效率优先


4.4 实际体验对比(redis-cli)

bash 复制代码
# StringRedisSerializer 存的:
GET user:1
"{\"name\":\"张三\",\"age\":25}"
# ✅ 一目了然

# JdkSerializationRedisSerializer 存的:
GET user:1
"\xac\xed\x00\x05sr\x00\x1acom.example.User\x8f\x12..."
# ❌ 完全看不懂

# Jackson2JsonRedisSerializer 存的:
GET user:1
"{\"name\":\"张三\",\"age\":25}"
# ✅ 一目了然

# GenericJackson2JsonRedisSerializer 存的:
GET user:1
"{\"@class\":\"com.example.User\",\"name\":\"张三\",\"age\":25}"
# ✅ 可读,但多了 @class

五、生产环境推荐方案

5.1 推荐:StringRedisTemplate + 手动 JSON

java 复制代码
@Configuration
public class RedisConfig {

    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
        return new StringRedisTemplate(factory);
    }
}
java 复制代码
@Service
public class UserService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    // 缓存用户
    public void cacheUser(User user) {
        String json = JSON.toJSONString(user);
        redisTemplate.opsForValue().set("user:" + user.getId(), json, 30, TimeUnit.MINUTES);
    }

    // 读缓存
    public User getCachedUser(Long id) {
        String json = redisTemplate.opsForValue().get("user:" + id);
        if (json == null) return null;
        return JSON.parseObject(json, User.class);
    }

    // 缓存验证码
    public void cacheSmsCode(String phone, String code) {
        redisTemplate.opsForValue().set("sms:" + phone, code, 5, TimeUnit.MINUTES);
    }

    // 分布式锁
    public boolean tryLock(String key, String value, long seconds) {
        return Boolean.TRUE.equals(
            redisTemplate.opsForValue().setIfAbsent("lock:" + key, value, seconds, TimeUnit.SECONDS)
        );
    }
}

5.2 备选:RedisTemplate + GenericJackson2JsonRedisSerializer

java 复制代码
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);

        GenericJackson2JsonRedisSerializer jsonSerializer =
            new GenericJackson2JsonRedisSerializer();
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);

        return template;
    }
}

5.3 什么时候用哪个?

场景 推荐方案 原因
验证码、token、锁、计数器 StringRedisTemplate value 本身就是 String
缓存对象(高 QPS) StringRedisTemplate + 手动 JSON 性能好、可控
缓存对象(开发效率优先) GenericJackson2Json 省事,自动转
多语言共用 Redis StringRedisTemplate + 手动 JSON 无 @class,通用
纯 Java、对象类型多 GenericJackson2Json 自动识别类型
只缓存一种对象 Jackson2JsonRedisSerializer 可以,但场景少见
任何场景 JdkSerialization 不推荐

六、为什么缓存推荐 StringRedisSerializer?

原因 1:缓存的 value 大多数情况本来就是 String

java 复制代码
redisTemplate.opsForValue().set("sms:code:13800138000", "886521");     // 验证码
redisTemplate.opsForValue().set("token:user:1001", "eyJhbGciOi...");   // JWT token
redisTemplate.opsForValue().set("lock:order:2001", "uuid-xxx");        // 分布式锁
redisTemplate.opsForValue().set("counter:page:home", "15234");         // 计数器
redisTemplate.opsForValue().set("config:timeout", "3000");             // 配置项

用 Jackson 存一个 "886521"?杀鸡用牛刀。

原因 2:性能

复制代码
StringRedisSerializer:
    "886521".getBytes("UTF-8")     // 纳秒级,几乎零开销

Jackson2JsonRedisSerializer:
    1. 反射获取 User 的所有字段
    2. 逐个字段序列化成 JSON
    3. .getBytes("UTF-8")
    // 微秒级,复杂对象可能毫秒级

缓存追求的就是。高 QPS 场景下,序列化开销不可忽视。

原因 3:透明可控,没有"魔法"

java 复制代码
// Jackson 自动序列化,你不确定它到底存了什么:
redisTemplate.opsForValue().set("user:1", user);
// null 字段怎么处理?日期格式是什么?循环引用怎么办?你得去猜。

// StringRedisSerializer,你自己转,你完全清楚:
String json = JSON.toJSONString(user);
// 所见即所得。

原因 4:一个 template 存任意类型

java 复制代码
// StringRedisTemplate:想存什么存什么
redisTemplate.opsForValue().set("user:1", JSON.toJSONString(user));
redisTemplate.opsForValue().set("order:1", JSON.toJSONString(order));
redisTemplate.opsForValue().set("config", "some_string_value");
// 全部 OK,不用纠结 serializer 配什么 class

// Jackson2JsonRedisSerializer<User>:只能存 User
// 想存 Order?再建一个 template?

原因 5:避免 Object.class 的 LinkedHashMap 坑

java 复制代码
// Jackson2JsonRedisSerializer<>(Object.class) 读出来:
Object obj = redisTemplate.opsForValue().get("user:1");
// obj 实际是 LinkedHashMap!不是 User!
// {name=张三, age=25}  ← LinkedHashMap

// StringRedisSerializer + 手动转:
User user = JSON.parseObject(json, User.class);
// 你明确指定了类型,不会出错

原因 6:跨语言兼容

java 复制代码
// 你自己转的纯 JSON:
{"name":"张三","age":25}
// Python、Go、Node.js 都能直接解析

// GenericJackson2Json 存的:
{"@class":"com.example.User","name":"张三","age":25}
// @class 对非 Java 语言毫无意义

原因 7:避免序列化的各种坑

java 复制代码
// Jackson 自动序列化的常见坑:
// 坑1:没有无参构造函数 → 反序列化失败
// 坑2:字段是接口类型(List<Animal>)→ 不知道具体类
// 坑3:循环引用(A 引用 B,B 引用 A)→ 栈溢出
// 坑4:LocalDateTime 格式不对 → 解析报错
// 坑5:泛型擦除 → List<User> 反序列化出来是 List<LinkedHashMap>

// 用 StringRedisSerializer + 手动转:
// 这些问题你在转 JSON 的时候就发现了,不会等到读缓存时才炸。

七、完整数据流转图

复制代码
┌─────────────────────────────────────────────────────────────┐
│                      你的 Java 代码                          │
│   User user = new User("张三", 25);                         │
│   String json = JSON.toJSONString(user);                    │
│   redisTemplate.opsForValue().set("user:1", json);          │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│           StringRedisSerializer(序列化器)                    │
│   String → .getBytes("UTF-8") → byte[]                     │
│   {"name":"张三","age":25} → [7B 22 6E 61 ...]             │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                    TCP 网络传输                               │
│              全是 byte[] 字节流                               │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│                      Redis 服务端                            │
│   redisObject { type=STRING, encoding=EMBSTR }              │
│   SDS { len=27, buf=[7B 22 6E 61 6D 65 ...] }              │
│   就是一块连续的 byte[],不关心内容是什么                      │
└─────────────────────────────────────────────────────────────┘

八、一句话总结

Redis 底层只认 byte\[\]。所有的高级类型(String、Object、JSON)都是客户端/序列化器在应用层做的转换。

生产环境推荐 StringRedisTemplate + 手动 JSON。多写两行代码,换来的是:性能、透明、可控、跨语言、无坑。

JDK 序列化是 RedisTemplate 的默认值,但那是 Spring 偷懒没改默认配置,不代表推荐。忘掉它。

相关推荐
这就是佬们吗20 小时前
回溯算法三板斧---掌握「回溯三问」思考模板快速入门回溯
java·数据库·算法
青皮桔20 小时前
Redis AOF 文件损坏修复记录
运维·数据库·redis
happyh h h h p p p p20 小时前
LVS 项目完整知识点总结
linux·服务器·数据库
仙宇觉尘21 小时前
记一次 .NET 某中医药附属医院门诊系统 崩溃分析
数据库·oracle·.net
小码农 - 初21 小时前
MySQL的表操作
数据库·mysql
灵机一物21 小时前
2026智能清洁设备推荐榜:无人值守与效率提升解析
数据库
xuankuxiaoyao21 小时前
WQ 算子尝试1
数据库·mysql
筑梦之路21 小时前
harbor使用国产数据库PolarDB PG版本部署——筑梦之路
数据库·信创适配·国产化改造
大鹏说大话1 天前
移动应用打包常见报错汇总
数据库
数安3000天1 天前
全行“手机号“字段有20多个名字——字段命名不统一,数据分类分级怎么做?
大数据·数据库·分类