我不会起名字322 · 后端 / 算法 / 数据库
文章目录
- [Redis 从 0 到 1(二十七):JSON 体积是 Protobuf 的 3.7 倍?3 种序列化的 Go 基准测试](#Redis 从 0 到 1(二十七):JSON 体积是 Protobuf 的 3.7 倍?3 种序列化的 Go 基准测试)
-
- [一、现象:1 亿条缓存,序列化格式多花 20GB](#一、现象:1 亿条缓存,序列化格式多花 20GB)
- [二、Go 实操:Benchmark 同一份数据(收藏点 1)](#二、Go 实操:Benchmark 同一份数据(收藏点 1))
- [三、统一 Codec 封装(收藏点 2)](#三、统一 Codec 封装(收藏点 2))
- [四、别忘了 Redis 侧的验证](#四、别忘了 Redis 侧的验证)
- 五、排查步骤:缓存内存与估算不符
- 六、总结
Redis 从 0 到 1(二十七):JSON 体积是 Protobuf 的 3.7 倍?3 种序列化的 Go 基准测试
上一篇《Redis 从 0 到 1(二十六):INFO memory 里 8 个关键数字------内存构成与容量规划》我们搞清了内存花在哪,链接:https://blog.csdn.net/2501_92769340/article/details/167400142 。
本篇优化"数据本身的体积":缓存对象用什么格式序列化,直接决定内存和网络的双重开销。
一、现象:1 亿条缓存,序列化格式多花 20GB
用户画像缓存:单条 2KB(JSON)。技术负责人拍板"JSON 可读性好,出问题好排查"。账是这样算的:1 亿用户 × 2KB = 200GB 内存。而如果换二进制格式(Protobuf 约 550B),同样的数据只要 55GB------差了 3.7 倍,一台机器变三台。
可读性可以用工具补(protobuf 有反射和 debug 工具),内存差 3.7 倍没法补。本篇用 Go 的 Benchmark 把三种格式摆到同一擂台上。
二、Go 实操:Benchmark 同一份数据(收藏点 1)
go
package main
import (
"encoding/json"
"testing"
"time"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/vmihailenco/msgpack/v5"
)
type UserProfile struct {
ID int64 `json:"id" msgpack:"id"`
Name string `json:"name" msgpack:"name"`
Email string `json:"email" msgpack:"email"`
Avatar string `json:"avatar" msgpack:"avatar"`
Tags []string `json:"tags" msgpack:"tags"`
Score int64 `json:"score" msgpack:"score"`
CreatedAt time.Time `json:"created_at" msgpack:"created_at"`
}
func sample() *UserProfile {
return &UserProfile{
ID: 1001, Name: "zhangsan", Email: "zs@example.com",
Avatar: "https://cdn.example.com/a/b/c.jpg",
Tags: []string{"vip", "active", "beta", "beijing", "2026"},
Score: 987654, CreatedAt: time.Now(),
}
}
func TestSize(t *testing.T) {
u := sample()
j, _ := json.Marshal(u)
m, _ := msgpack.Marshal(u)
pb, _ := proto.Marshal(&UserProfilePB{ // 由 protoc 生成的类型
Id: 1001, Name: "zhangsan", Email: "zs@example.com",
Avatar: "https://cdn.example.com/a/b/c.jpg",
Tags: []string{"vip", "active", "beta", "beijing", "2026"},
Score: 987654, CreatedAt: timestamppb.Now(),
})
t.Logf("JSON=%dB MessagePack=%dB Protobuf=%dB (JSON/PB=%.1f倍)",
len(j), len(m), len(pb), float64(len(j))/float64(len(pb)))
}
func BenchmarkJSONEncode(b *testing.B) {
u := sample()
for i := 0; i < b.N; i++ {
json.Marshal(u)
}
}
func BenchmarkMsgpackEncode(b *testing.B) {
u := sample()
for i := 0; i < b.N; i++ {
msgpack.Marshal(u)
}
}
func BenchmarkProtobufEncode(b *testing.B) {
pb := &UserProfilePB{Id: 1001, Name: "zhangsan"} // 预构造
for i := 0; i < b.N; i++ {
proto.Marshal(pb)
}
}
// Decode 基准同法:BenchmarkJSONDecode 等
本机典型结果(数字随数据形态波动,量级稳定):
| 格式 | 单条体积 | 编码耗时(相对值) | 解码耗时 | 可读性 |
|---|---|---|---|---|
| JSON | 100% | 100% | 100% | ✅ |
| MessagePack | ~60% | ~70% | ~80% | ❌(二进制) |
| Protobuf | ~27% | ~40% | ~55% | ❌(二进制,有描述文件) |
三、统一 Codec 封装(收藏点 2)
业务代码不应该散落 json.Marshal------换成统一的 Codec 接口,选型只改一行配置:
go
package codec
import "context"
type Codec interface {
Marshal(v any) ([]byte, error)
Unmarshal(data []byte, v any) error
}
// 缓存读写的统一入口:换序列化格式只需换构造参数
func SetObject(ctx context.Context, rdb *redis.Client, key string, v any, ttl time.Duration, c Codec) error {
data, err := c.Marshal(v)
if err != nil {
return err
}
return rdb.SetEx(ctx, key, data, ttl).Err()
}
func GetObject(ctx context.Context, rdb *redis.Client, key string, v any, c Codec) error {
data, err := rdb.Get(ctx, key).Bytes()
if err != nil {
return err
}
return c.Unmarshal(data, v)
}
选型建议表(收藏点 3):
| 场景 | 推荐 | 理由 |
|---|---|---|
| 对象缓存(内存敏感) | Protobuf | 体积最小、编解码快 |
| 短期缓存、结构多变 | MessagePack | 免 .proto 维护,schemaless |
| 配置类、人工排查频繁 | JSON | 可读性优先,体积不敏感 |
| 兼容旧系统 | JSON | 跨语言零成本 |
四、别忘了 Redis 侧的验证
bash
# 写入后验证体积变化
STRLEN cache:user:1001 # 看 value 字节数
MEMORY USAGE cache:user:1001 # 看含结构的完整开销
大 value(>1KB)注意:第 22 篇的 bigkey 阈值是相对的------全实例都是 2KB 的 value、千万级 key,就是"普遍偏大"而不是个别 bigkey,考虑用第 26 篇的 listpack 思路把对象拆字段存 Hash。
五、排查步骤:缓存内存与估算不符
STRLEN抽样看实际 value 大小;- 发现比预期大 3 倍以上 → 检查是不是有人往 value 里塞了调试字段(JSON 最容易膨胀);
- 统一 Codec 入口排查:全局搜
json.Marshal,确认缓存路径都走了 Codec; - 评估迁移:旧数据无法原地转格式,双写新格式 + 灰度读,切完停旧。
六、总结
- JSON 的可读性成本是 3.7 倍体积和 2 倍编解码 CPU,大基数缓存扛不住;
- Protobuf 体积最小、最快,但要维护 .proto;MessagePack 是免 schema 的折中;
- Codec 接口统一入口,换格式不改业务代码;
- 体积变化用 STRLEN + MEMORY USAGE 在 Redis 侧验证。
下一篇预告:存储层优化完了,下一篇把客户端韧性补齐------下一篇《Redis 抖一下,重试把它打挂了------超时/重试/熔断的 Go 封装》,我们给调用链装上"保险丝"。