序列化与反序列化:选型、对比与实战
大家好,我是你们的架构课讲师。今天我们来聊聊分布式系统里一个绕不开的话题——序列化。
说白了,序列化就是把内存里的对象,变成一串能存能传的字节。反序列化就是反过来。你想想看,微服务之间要通信,数据要落盘,没有序列化,啥都干不了。
我这些年做过的项目,从早期的单体应用,到后来的大型分布式系统,序列化框架换了好几茬。踩过的坑,说实话,比吃过的盐还多。今天就把这些经验掰开揉碎了讲给你听。
为什么序列化这么重要?
很多人觉得序列化不就是把对象转成字节吗?随便选一个不就行了?
嗯,这里要注意。在分布式系统里,序列化直接影响三件事:
- 性能:序列化快不快,字节数多不多,直接决定了你的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虽然好,但生成的代码还是有点重。
协议设计要点
一个简单的自定义协议,通常包含这几部分:
- 魔数:2字节,用于快速校验数据格式。比如0xCAFE。
- 版本号:1字节,用于协议升级。
- 序列化方式:1字节,比如0表示JSON,1表示Protobuf,2表示自定义。
- 数据长度:4字节,表示正文的长度。
- 正文:变长,真正的序列化数据。
举个例子:
+--------+--------+--------+--------+------------------+
| 魔数 | 版本 | 类型 | 长度 | 正文 |
| 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