Avro 序列化基础:Avro 简介、Schema 定义、基本类型与复杂类型
好,咱们正式开始聊 Avro。说实话,在接触 Schema Registry 之前,我最早用 Avro 纯粹是因为它省空间。那时候公司日志量一天几个 TB,JSON 存得我肉疼。后来才发现,Avro 真正的杀手锏不是压缩比,而是它自带的 Schema 机制——数据自描述,这才是它能跟 Schema Registry 完美配合的根本原因。
Avro 是什么?
Avro 是 Apache 搞的一个序列化框架。说白了,就是把内存里的对象转成二进制流,或者反过来。跟 JSON、Protobuf、Thrift 干的是同一件事。但它有几个特点,让我个人觉得特别顺手:
- Schema 是 JSON 格式的——你写起来不费劲,读起来也直观。
- 数据自带 Schema——反序列化时不需要额外传一个定义文件,文件头里就嵌着。
- 支持 Schema 演化——字段增删改,前后兼容,这是后面 Schema Registry 能玩转的基础。
- 没有 IDL 编译器——动态类型语言直接用,静态语言也有代码生成工具,但你不生成也能跑。
核心要点:Avro 的序列化结果 = Schema(JSON 格式)+ 数据(二进制格式)。这个结构决定了它天生适合做跨语言、跨版本的数据交换。
Schema 定义:从 JSON 开始
Avro Schema 本身就是一个 JSON 字符串。你定义一个对象,用 JSON 描述它的字段名、字段类型、默认值。我习惯先写一个简单的例子,比如用户信息:
{
"type": "record",
"name": "User",
"namespace": "com.example",
"fields": [
{"name": "id", "type": "int"},
{"name": "name", "type": "string"},
{"name": "email", "type": ["null", "string"], "default": null}
]
}
你看,这个 Schema 描述了一个叫 User 的记录,有三个字段。其中 email 用了联合类型 ["null", "string"],表示它可以是 null 也可以是字符串。我建议你养成给联合类型加默认值的习惯,否则演化时会出问题。
小技巧:写 Schema 时,namespace 最好带上。跨项目引用时,全限定名能避免命名冲突。我在一个多团队协作的项目里吃过这个亏——两个团队都定义了 Order,结果反序列化时全乱了。
基本类型:Avro 的九种原语
Avro 的基本类型不多,就九个。我列个表,你一眼就能看明白:
| 类型 | 说明 | JSON 示例 |
|---|---|---|
null |
空值 | null |
boolean |
布尔值 | true / false |
int |
32 位有符号整数 | 42 |
long |
64 位有符号整数 | 1234567890L |
float |
32 位浮点数 | 3.14 |
double |
64 位浮点数 | 3.1415926 |
bytes |
字节序列 | "\u00FF" |
string |
UTF-8 编码字符串 | "hello" |
嗯,这里要注意:int 和 long 在序列化时用的是变长编码。值越小,占的字节越少。所以如果你存的是小数字,用 int 比 long 更省空间。我在做 IoT 设备数据采集时,温度值基本在 -50 到 100 之间,用 int 平均只占 1-2 个字节,比固定 4 字节省了一半多。
复杂类型:组合出你的数据结构
基本类型只能描述简单值。真实业务里,我们需要的是嵌套、数组、枚举这些东西。Avro 提供了六种复杂类型:
Record(记录)
最常用的复杂类型,相当于结构体或类。刚才的 User 就是一个 Record。你可以嵌套 Record:
{
"type": "record",
"name": "Order",
"fields": [
{"name": "orderId", "type": "string"},
{"name": "user", "type": "User"},
{"name": "items", "type": {"type": "array", "items": "Item"}}
]
}
Enum(枚举)
固定取值集合。比如订单状态:
{
"type": "enum",
"name": "OrderStatus",
"symbols": ["PENDING", "PAID", "SHIPPED", "DELIVERED", "CANCELLED"]
}
注意:枚举的 symbols 顺序一旦发布,就不能随意调整。我曾经在重构时重新排了枚举顺序,结果旧数据反序列化时全部错位——枚举是按索引存的,不是按名称。这个坑我踩过,你别踩。
Array(数组)
可变长度的同类型元素列表。语法是 {"type": "array", "items": "int"}。数组元素可以是任何类型,包括 Record 或其他复杂类型。
Map(映射)
键值对集合,键必须是 string,值可以是任意类型。比如:
{"type": "map", "values": "double"}
这个定义了一个从字符串到双精度浮点数的映射。我在做配置中心时,用 Map 存了一堆动态参数,省去了频繁改 Schema 的麻烦。
Union(联合)
表示字段可以是多种类型之一。语法是 JSON 数组,比如 ["null", "string"]。联合类型的顺序很重要——默认值必须匹配第一个类型。我建议你把 null 放第一位,这样默认值就是 null,演化时最安全。
Fixed(固定长度)
固定字节数,适合存哈希值、UUID 等。比如:
{"type": "fixed", "name": "MD5Hash", "size": 16}
知识体系总览
下面这张图把 Avro 的类型体系串起来了。你看着它,就能快速回忆整个结构:
实战:写一个完整的 Schema
咱们来写一个稍微复杂点的例子。假设你在做一个电商系统,需要定义订单数据结构:
{
"type": "record",
"name": "Order",
"namespace": "com.example.ecommerce",
"fields": [
{"name": "orderId", "type": "string"},
{"name": "userId", "type": "int"},
{"name": "status", "type": {"type": "enum", "name": "OrderStatus", "symbols": ["CREATED", "PAID", "SHIPPED", "DELIVERED", "CANCELLED"]}},
{"name": "items", "type": {"type": "array", "items": {
"type": "record",
"name": "OrderItem",
"fields": [
{"name": "productId", "type": "int"},
{"name": "quantity", "type": "int"},
{"name": "price", "type": "double"}
]
}}},
{"name": "totalAmount", "type": "double"},
{"name": "shippingAddress", "type": ["null", "string"], "default": null},
{"name": "createdAt", "type": "long"}
]
}
这个 Schema 用到了 Record、Enum、Array、Union 四种复杂类型。你想想看,如果用 JSON 存同样的数据,体积至少大 3 倍。而且 Avro 的二进制格式解析速度极快——不需要像 JSON 那样逐个字符解析。
个人建议:刚开始用 Avro 时,别急着写复杂的嵌套。先定义几个简单的 Record,跑通序列化/反序列化流程,再逐步加复杂类型。我在第一个 Avro 项目里直接上了四层嵌套,结果调试 Schema 演化时把自己绕晕了。
避坑指南
最后分享几个我踩过的坑:
- 字段顺序不能乱改——Avro 按字段顺序编码,不是按字段名。你改了顺序,旧数据就废了。加新字段只能追加到末尾。
- 联合类型默认值必须匹配第一个类型——比如
["null", "string"]的默认值必须是null,不能是""。 - 枚举符号不能删除——只能追加,而且只能追加到末尾。删掉一个符号,所有用这个枚举的旧数据都会反序列化失败。
- Fixed 类型的长度一旦定义就不能改——改长度相当于换了一个新类型,前后完全不兼容。
嗯,Avro 的基础就这些。你把这些类型和 Schema 定义搞熟了,后面讲 Schema Registry 的兼容性策略时,你就能理解为什么有些策略能过、有些过不了。说白了,Avro 的 Schema 演化规则,就是 Schema Registry 的底层逻辑。