Redis 从 0 到 1(二十七):JSON 体积是 Protobuf 的 3.7 倍?3 种序列化的 Go 基准测试

我不会起名字322 · 后端 / 算法 / 数据库

📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL

文章目录

  • [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。

五、排查步骤:缓存内存与估算不符

  1. STRLEN 抽样看实际 value 大小;
  2. 发现比预期大 3 倍以上 → 检查是不是有人往 value 里塞了调试字段(JSON 最容易膨胀);
  3. 统一 Codec 入口排查:全局搜 json.Marshal,确认缓存路径都走了 Codec;
  4. 评估迁移:旧数据无法原地转格式,双写新格式 + 灰度读,切完停旧。

六、总结

  1. JSON 的可读性成本是 3.7 倍体积和 2 倍编解码 CPU,大基数缓存扛不住;
  2. Protobuf 体积最小、最快,但要维护 .proto;MessagePack 是免 schema 的折中;
  3. Codec 接口统一入口,换格式不改业务代码;
  4. 体积变化用 STRLEN + MEMORY USAGE 在 Redis 侧验证。

下一篇预告:存储层优化完了,下一篇把客户端韧性补齐------下一篇《Redis 抖一下,重试把它打挂了------超时/重试/熔断的 Go 封装》,我们给调用链装上"保险丝"。

相关推荐
我不会起名字32234 分钟前
Redis 从 0 到 1(二十六):INFO memory 里 8 个关键数字——内存构成与容量规划
redis·容量规划·内存·info·listpack
我不会起名字3221 小时前
Redis 从 0 到 1(二十五):连接池又打满了——PoolSize 公式、泄漏排查与 PoolStats
redis·连接池·go-redis·排查·poolstats
我不会起名字3221 小时前
Redis 从 0 到 1(二十三):一个热 key 打到 10 万 QPS——hotkey 与本地缓存的 Go 实现
redis·缓存·go·qps·hotkey
IT规划师2 小时前
Redis 数据类型全解 - Bitmap 生产实战:把位视图讲准、用对、守住红线(六)
redis·.net
Patrick_Wilson14 小时前
Go 为什么需要 defer?从函数怎么收尾说起
后端·go·编程语言
Fatbobman(东坡肘子)14 小时前
快速消失的硬盘容量 -- 肘子的 Swift 周报 #160
开发语言·人工智能·redis·ios·swiftui·iphone·swift
Patrick_Wilson14 小时前
从文本输出理解 Go 方法与接口
后端·go·编程语言
温暖小土15 小时前
在 CentOS 上安装 Redis Stack
linux·redis·centos