序列化与反序列化:选型、对比与实战

大家好,我是你们的架构课讲师。今天我们来聊聊分布式系统里一个绕不开的话题——序列化。

说白了,序列化就是把内存里的对象,变成一串能存能传的字节。反序列化就是反过来。你想想看,微服务之间要通信,数据要落盘,没有序列化,啥都干不了。

我这些年做过的项目,从早期的单体应用,到后来的大型分布式系统,序列化框架换了好几茬。踩过的坑,说实话,比吃过的盐还多。今天就把这些经验掰开揉碎了讲给你听。

为什么序列化这么重要?

很多人觉得序列化不就是把对象转成字节吗?随便选一个不就行了?

嗯,这里要注意。在分布式系统里,序列化直接影响三件事:

  • 性能:序列化快不快,字节数多不多,直接决定了你的RPC调用延迟和网络带宽消耗。
  • 兼容性:服务升级了,字段变了,老客户端还能不能正常通信?
  • 跨语言能力:你的订单服务用Java,支付服务用Go,它们之间怎么交换数据?

我在一个电商项目中,就因为序列化选型失误,导致双11大促时CPU飙升,最后紧急回滚。那次教训太深刻了。

主流序列化方案对比

JSON:简单但低效

JSON是大家最熟悉的。可读性好,调试方便。但它的缺点也很明显:

  • 体积大:字段名重复出现,比如{"userId": 123, "userName": "张三"},每个字段名都带着引号。
  • 解析慢:字符串解析,类型转换,开销不小。
  • 无类型约束:你传个数字,对方可能收到字符串,得自己处理。

我的建议:JSON适合做配置、日志、对外API。内部服务间通信,尽量别用。

Protobuf:Google的利器

Protobuf是我个人最常用的方案。它用.proto文件定义数据结构,然后生成各语言的代码。

优点很明显:

  • 体积小:二进制编码,没有冗余字段名。同样的数据,比JSON小60%-80%。
  • 速度快:序列化和反序列化都是直接操作二进制,比JSON快一个数量级。
  • 强类型:字段类型、顺序都是固定的,不会出现类型混乱。
  • 向前向后兼容:加字段、删字段,只要遵循规范,老版本也能处理。

我记得有一次,我们把一个核心服务的序列化从JSON换成Protobuf,接口延迟直接降了40%。

小技巧:Protobuf的字段编号1-15占用1个字节,16-2047占用2个字节。所以常用的字段,编号尽量用1-15。

Thrift:Facebook的全面方案

Thrift和Protobuf很像,但它不止是序列化框架,还包含了完整的RPC框架。

Thrift支持多种传输协议和传输方式,灵活性更高。但它的缺点是:

  • 学习成本高:概念多,配置复杂。
  • 社区活跃度不如Protobuf:现在用的人相对少一些。

我个人觉得,如果你需要一个完整的RPC方案,可以考虑Thrift。如果只是序列化,Protobuf更轻量。

序列化性能测试

光说不练假把式。我们来做个简单的性能测试对比。

测试条件:同样的用户对象(10个字段),序列化100万次,取平均耗时。

方案 序列化耗时(ms) 反序列化耗时(ms) 字节大小(bytes)
JSON (Jackson) 185 220 128
Protobuf 42 38 32
Thrift (Binary) 55 52 36

看到了吧?Protobuf在速度和体积上都有明显优势。Thrift稍逊一筹,但也远好于JSON。

避坑指南:我曾经见过一个团队,用JSON序列化大对象(几百个字段),结果每次序列化都要几十毫秒,接口直接超时。后来换成Protobuf,问题迎刃而解。

自定义序列化协议设计

有时候,现成的方案满足不了你的需求。比如,你需要极致的性能,或者特殊的编码规则。这时候,就得自己设计序列化协议了。

我设计过一个自定义协议,用在物联网网关项目里。设备端资源有限,CPU和内存都很紧张。Protobuf虽然好,但生成的代码还是有点重。

协议设计要点

一个简单的自定义协议,通常包含这几部分:

  1. 魔数:2字节,用于快速校验数据格式。比如0xCAFE。
  2. 版本号:1字节,用于协议升级。
  3. 序列化方式:1字节,比如0表示JSON,1表示Protobuf,2表示自定义。
  4. 数据长度:4字节,表示正文的长度。
  5. 正文:变长,真正的序列化数据。

举个例子:

+--------+--------+--------+--------+------------------+
| 魔数   | 版本   | 类型   | 长度   | 正文             |
| 2字节  | 1字节  | 1字节  | 4字节  | 变长             |
+--------+--------+--------+--------+------------------+

代码示例

下面是一个简单的自定义序列化实现,只支持基本类型:

public class CustomSerializer {

    public byte[] serialize(Object obj) throws Exception {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bos);

        // 写入魔数
        dos.writeShort(0xCAFE);
        // 写入版本
        dos.writeByte(1);
        // 写入序列化类型
        dos.writeByte(0);
        // 预留长度位置
        dos.writeInt(0);

        // 序列化字段
        if (obj instanceof User) {
            User user = (User) obj;
            dos.writeLong(user.getId());
            writeString(dos, user.getName());
            dos.writeInt(user.getAge());
        }

        // 回写长度
        byte[] data = bos.toByteArray();
        int length = data.length - 8; // 减去头部8字节
        // 用ByteBuffer修改长度字段
        ByteBuffer buffer = ByteBuffer.wrap(data);
        buffer.putInt(4, length);

        return data;
    }

    private void writeString(DataOutputStream dos, String str) throws Exception {
        byte[] bytes = str.getBytes("UTF-8");
        dos.writeInt(bytes.length);
        dos.write(bytes);
    }
}

提示:自定义协议虽然灵活,但维护成本高。除非有特殊需求,否则还是优先用Protobuf。

总结

好了,今天的内容就到这里。我们来回顾一下:

  • JSON:适合调试和对外接口,性能一般。
  • Protobuf:性能好,体积小,兼容性强,是我最推荐的方案。
  • Thrift:功能全面,但学习成本高。
  • 自定义协议:适合极端场景,但慎用。

最后说一句,序列化选型没有银弹。你得根据你的业务场景、性能要求、团队技术栈来综合判断。别盲目跟风,也别固步自封。

下一章,我们会聊聊RPC框架的核心原理。到时候见。

公众号:蓝海资料掘金营,微信deep3321