序列化与反序列化

在 Maomi.MQ 里,消息体最终都是 byte[]。序列化器的作用不是“可有可无”,它会直接影响:

  • 网络包大小(带宽、RabbitMQ 存储压力)
  • 发布/消费 CPU 开销
  • 语言兼容性(是否容易跨语言)
  • 模型演进成本(字段增删改后是否稳定)

这章不只列性能数据,还会讲清楚怎么选、怎么配、怎么避坑。

支持的序列化器

Maomi.MQ 当前支持 5 种:

  1. DefaultJsonMessageSerializer(默认内置)
  2. MessagePackSerializer
  3. ProtobufMessageSerializer(Google.Protobuf)
  4. ProtobufMessageSerializer(protobuf-net)
  5. ThriftMessageSerializer

说明:AddMaomiMQ() 默认会注册 JSON 序列化器。其它序列化器需要你手动加入到 MessageSerializers 列表。

性能基准(你现在看到的表)

下面是仓库 Benchmark 的结果(小对象 + 大对象)。这份数据的价值不是“选最快”,而是让你看到不同方案的趋势:

  • 二进制协议(Protobuf / MessagePack)通常比 JSON 更快、更省。
  • 模型变大后,差距会进一步拉开。
  • Thrift 在这份测试里分配和耗时都偏高,不适合追求极致吞吐的热点路径。
MethodMeanErrorStdDevRatioRatioSDGen0Gen1AllocatedAlloc Ratio
Protobuf_Serialize35.03 ns0.265 ns0.235 ns0.410.000.0102-128 B1.60
MessagePack_Serialize42.23 ns0.302 ns0.252 ns0.490.000.0044-56 B0.70
Protobuf_Deserialize57.40 ns1.058 ns0.938 ns0.670.010.0306-384 B4.80
MessagePack_Deserialize68.83 ns1.036 ns0.918 ns0.800.010.0159-200 B2.50
DefaultJson_Serialize86.14 ns0.284 ns0.252 ns1.000.000.0063-80 B1.00
ProtobufNet_Serialize105.47 ns1.590 ns1.328 ns1.220.020.0324-408 B5.10
DefaultJson_Deserialize128.35 ns2.338 ns4.391 ns1.510.040.0114-144 B1.80
ProtobufNet_Deserialize198.78 ns2.120 ns1.879 ns2.310.030.0114-144 B1.80
Protobuf_Large100_Serialize276.36 ns1.599 ns1.417 ns3.210.020.0296-376 B4.70
Thrift_Serialize295.00 ns4.794 ns4.250 ns3.420.050.29330.00243680 B46.00
Protobuf_Large100_Deserialize436.86 ns5.268 ns4.670 ns5.070.050.0715-904 B11.30
Thrift_Deserialize466.01 ns3.638 ns3.225 ns5.410.030.0486-616 B7.70
ProtobufNet_Large100_Serialize628.81 ns5.865 ns4.898 ns7.300.060.0544-688 B8.60
MessagePack_Large100_Serialize634.17 ns3.522 ns3.294 ns7.360.040.0496-632 B7.90
MessagePack_Large100_Deserialize972.86 ns10.396 ns9.215 ns11.290.110.0820-1048 B13.10
DefaultJson_Large100_Serialize1,048.01 ns4.930 ns4.611 ns12.170.060.0801-1024 B12.80
ProtobufNet_Large100_Deserialize1,950.35 ns27.716 ns25.925 ns22.620.290.0305-416 B5.20
Thrift_Large100_Serialize3,007.18 ns37.515 ns31.327 ns34.900.410.3891-4888 B61.10
DefaultJson_Large100_Deserialize4,559.64 ns53.891 ns45.001 ns52.910.570.0305-416 B5.20
Thrift_Large100_Deserialize9,295.34 ns128.772 ns114.153 ns107.911.250.1678-2184 B27.30

image-20260211144408992

怎么配置序列化器

默认只有 JSON。要启用其它协议,在 AddMaomiMQ 里把序列化器插入列表。

builder.Services.AddMaomiMQ(
    (MqOptionsBuilder options) =>
    {
        options.WorkId = 1;
        options.AppName = "myapp";

        options.MessageSerializers = serializers =>
        {
            // 插在前面,优先匹配
            serializers.Insert(0, new ProtobufMessageSerializer());
        };

        options.Rabbit = rabbit =>
        {
            rabbit.HostName = "127.0.0.1";
            rabbit.Port = 5672;
            rabbit.UserName = "guest";
            rabbit.Password = "guest";
        };
    },
    [typeof(Program).Assembly]);

配置顺序很重要

Maomi.MQ 的选择逻辑是:

  • 发布时:按 MessageSerializers 顺序,找到第一个 SerializerVerify(...) == true 的序列化器。
  • 消费时:按 MessageHeader.ContentType 找反序列化器。

所以,序列化器的顺序和 ContentType 设计都会影响行为。

每种序列化器的模型要求

JSON(默认)

  • 不需要额外标注。
  • 最容易调试(可读性高)。
  • 跨语言友好。
  • 体积和性能通常不如二进制协议。

适合:后台管理、低频任务、排障优先场景。

MessagePack

模型需要 MessagePack 标注,例如:

[MessagePackObject]
public sealed class OrderMessage
{
    [Key(0)] public string OrderNo { get; set; } = string.Empty;
    [Key(1)] public decimal Amount { get; set; }
}

特点:

  • 体积小、速度快。
  • C# 内部系统体验很好。
  • 跨语言也可用,但团队需要统一 schema 管理。

Google Protobuf

模型需实现 Google.Protobuf.IMessage(通常由 .proto 生成)。

特点:

  • 性能和体积表现优秀。
  • 跨语言生态成熟。
  • 适合高并发、跨团队/跨语言接口。

protobuf-net

模型需带 [ProtoContract][ProtoMember(n)]

[ProtoContract]
public sealed class PersonMessage
{
    [ProtoMember(1)] public Guid Id { get; set; }
    [ProtoMember(2)] public string Name { get; set; } = string.Empty;
}

特点:

  • 对纯 C# 团队友好,上手快。
  • 不强依赖 .proto 文件。
  • 在跨语言协作时,约束性通常不如 Google Protobuf 明确。

Thrift

模型需实现 TBase(通常来自 Thrift 代码生成)。

特点:

  • 适合历史系统已采用 Thrift 的场景。
  • 在当前 benchmark 里不是性能优选。

选型建议(实战)

如果你不确定怎么选,可以按下面决策:

  1. 首先看团队和生态:
  • 跨语言、多服务协作:优先 Google Protobuf。
  • C# 内部高性能:MessagePack 或 protobuf-net。
  • 兼容老系统:Thrift。
  1. 再看运维和排障:
  • 业务初期、经常抓包查问题:先 JSON,再逐步切二进制。
  1. 最后看热点路径:
  • 高频核心链路(下单、计费、风控):用二进制协议。
  • 低频链路(管理类、离线任务):JSON 通常足够。

常见坑与排查

找不到反序列化器

报错类似:
No suitable message serializer was found for content type 'xxx'

排查:

  • 发布端写入的 ContentType 与消费端是否都注册了对应序列化器。
  • 服务 A、B 的 MessageSerializers 配置是否一致。

模型不符合序列化器要求

例如:

  • MessagePack 模型没加 [MessagePackObject]
  • protobuf-net 模型没加 [ProtoContract]
  • Google Protobuf 不是 .proto 生成的 IMessage

这类错误通常会在发布或消费时抛 ArgumentException

同一 ContentType 冲突

Google Protobufprotobuf-net 默认都使用 application/x-protobuf

如果你同时注册两者,需要注意:

  • 消费端按 ContentType 选序列化器时,同一 ContentType 只会保留一个(按注册顺序取第一个)。
  • 最好在一个服务内固定只用一种 protobuf 方案,或自定义不同 ContentType 的序列化器实现。

发布端和消费端版本不一致

模型字段变更后,旧消费者可能读不到字段或语义变化。

建议:

  • 优先“追加字段”,避免复用字段号。
  • 升级时做灰度,发布端和消费端版本错开验证。

如何自定义序列化器

实现 IMessageSerializer 即可:

public class MySerializer : IMessageSerializer
{
    public string ContentType => "application/x-mycodec";

    public bool SerializerVerify<TObject>(TObject obj) => obj is MyMessage;
    public bool SerializerVerify<TObject>() => typeof(TObject) == typeof(MyMessage);

    public byte[] Serializer<TObject>(TObject obj)
    {
        // encode
        throw new NotImplementedException();
    }

    public TObject? Deserialize<TObject>(ReadOnlySpan<byte> bytes)
    {
        // decode
        throw new NotImplementedException();
    }
}

然后在 AddMaomiMQ 里加入:

options.MessageSerializers = serializers => serializers.Insert(0, new MySerializer());

一句话建议

  • 想省心:先 JSON。
  • 想性能:优先 Google Protobuf / MessagePack。
  • 想稳定演进:把 schema 管理当成“接口契约”来治理,而不是仅仅当作序列化细节。