从底层 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 层面 :你传
String或byte[]- 网络传输层面 :全是
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 偷懒没改默认配置,不代表推荐。忘掉它。