4. 序列化与反序列化:性能对比

序列化这事儿,在交易系统里太关键了。说白了,就是把内存里的对象变成字节流,通过网络发出去;反序列化就是反过来。你想想看,每一笔订单、每一次行情快照,都要走这个过程。如果这里慢了,整个系统的延迟就上去了。

我个人习惯,在选序列化方案时,会重点看三个维度:编码速度、解码速度、数据体积。今天咱们就把主流的几种方案拉出来遛遛。

4.1 JSON:最慢,但最方便

JSON 的好处不用多说,人类可读,调试方便。但做低延迟交易?我劝你慎重。

我在项目中遇到过,用 JSON 传行情快照,一个 1000 只股票的深度数据,序列化后体积能到 200KB 以上。解析的时候,光字符串转数字就要花不少时间。为什么?因为 JSON 本质是文本协议,每个数字都得做字符串到浮点数的转换,这个开销很大。

性能数据(单次操作,单位:微秒)

方案序列化反序列化体积(字节)
JSON12.518.2245
Protobuf3.14.889
FlatBuffers0.80.372
自定义二进制0.50.264

你看,JSON 比 Protobuf 慢了 4-5 倍。所以我的建议是:JSON 只适合做管理后台、日志输出,千万别用在交易链路上。

4.2 Protobuf:平衡之选

Protobuf 是 Google 搞的,用起来确实方便。定义个 .proto 文件,代码生成器一跑,啥都有了。

它的核心思路是:用编号代替字段名。比如字段名叫 "price",在 Protobuf 里就用编号 1 表示。这样体积就小了很多。

不过要注意,Protobuf 的编码方式叫 Varint,这个后面会细说。它有个特点:小数字占的字节少,大数字占的多。比如数字 1 只占 1 个字节,数字 100000 就要占 3 个字节。

我的经验:在定义 Protobuf 消息时,尽量把频繁出现的字段放在前面(编号小的位置)。这样编码效率会高一点点。虽然差别不大,但积少成多嘛。

4.3 FlatBuffers:零拷贝的王者

FlatBuffers 是 Google 的另一个作品。它最大的特点是:反序列化不需要解析。什么意思?就是你拿到字节流,直接就能读里面的字段,不需要先转成对象。

我记得第一次用 FlatBuffers 时,被它的速度吓了一跳。一个行情快照,Protobuf 反序列化要 4.8 微秒,FlatBuffers 只要 0.3 微秒。快了 16 倍!

为什么这么快?因为 FlatBuffers 把数据按固定偏移量排列。你读第 3 个字段,直接算一下偏移量,从那个位置读内存就行。没有循环、没有条件判断、没有内存分配。

注意:FlatBuffers 的缺点是写数据比较麻烦。你得先计算好每个字段的位置,然后按顺序写入。而且它不支持可变长度字段(比如变长数组),得用 vector 来模拟。

4.4 自定义二进制协议:极致性能

如果你对延迟的要求到了纳秒级,那就得自己动手了。自定义二进制协议,说白了就是:你定义每个字节的含义

比如一个订单消息,我可以这样设计:

// 自定义二进制协议示例
// 总长度:16 字节
// 字节 0-3:  订单ID (uint32)
// 字节 4-7:  价格 (int32, 单位:分)
// 字节 8-11: 数量 (uint32)
// 字节 12:   方向 (0=买, 1=卖)
// 字节 13-15: 保留

struct OrderMessage {
    uint32_t order_id;   // 4 字节
    int32_t  price;      // 4 字节
    uint32_t quantity;   // 4 字节
    uint8_t  side;       // 1 字节
    uint8_t  reserved[3]; // 3 字节
};

你看,这个结构体直接就是 16 字节。序列化就是 memcpy,反序列化也是 memcpy。没有比这更快的了。

我在项目中遇到过,用自定义协议处理行情数据,单次序列化只要 0.5 微秒。比 Protobuf 快了 6 倍。

核心要点:自定义协议的关键是 固定长度 + 固定偏移。每个字段的位置在编译时就确定了,运行时不需要任何计算。

4.5 Varint 编码:小数字的福音

Varint 是 Protobuf 和很多二进制协议用的编码方式。它的思想很简单:小数字用少字节,大数字用多字节

具体做法是:每个字节的最高位(第 8 位)作为标志位。如果最高位是 1,表示后面还有字节;如果是 0,表示这是最后一个字节。剩下的 7 位存数据。

举个例子,数字 300 的 Varint 编码:

// 数字 300 的二进制:100101100
// 分成 7 位一组:0000010 0101100
// 加上标志位:10000010 00101100
// 所以 Varint 编码是:0x82 0x2C

你看,300 这个数,正常用 uint32 要 4 个字节,Varint 只用 2 个字节。省了一半空间。

不过要注意,Varint 对 大数字不友好。比如 2^31 这个数,Varint 要 5 个字节,比 uint32 还多 1 个字节。所以如果你的数据里有很多大数字,Varint 反而会增大体积。

我的建议:在交易系统里,价格、数量这些字段通常不会太大(比如价格一般不超过 10 万),用 Varint 很合适。但订单 ID 这种可能很大的字段,建议用固定长度编码。

4.6 如何选择?

嗯,说了这么多,到底该用哪个?我个人的经验是:

  • 管理后台、日志:用 JSON,方便调试
  • 一般交易链路:用 Protobuf,平衡性能和易用性
  • 高频行情:用 FlatBuffers,零拷贝是王道
  • 核心订单链路:用自定义二进制协议,极致性能

我曾经在一个项目中,把行情链路的序列化从 JSON 换成 FlatBuffers,整体延迟从 50 微秒降到了 5 微秒。10 倍的提升,就改了一个模块。你说值不值?

最后提醒一句:序列化方案的选择,一定要结合你的数据特征。别盲目追求极致性能,也别图方便用 JSON。找到最适合你场景的方案,才是正道。