3. Avro Schema 编写实战:record、enum、array、map、union 类型详解与示例
好,咱们直接进入正题。上一章我们把 Avro 的基础概念和 Schema 的 JSON 骨架过了一遍。这一章,我带你手写几个最核心的类型。说白了,你只要把这五种类型玩熟了,日常 90% 的序列化场景都能搞定。
我个人习惯把 Avro 的类型分成两类:基础类型(string, int, long 这些)和 复杂类型(record, enum, array, map, union)。基础类型没啥好讲的,就是 JSON 里写个字段名加个类型名。复杂类型才是真正体现 Schema 设计能力的地方。
核心观点:Avro 的复杂类型不是用来炫技的,而是用来精确描述业务模型的。你写得越准确,下游消费方就越省心。
3.1 record 类型:一切模型的基石
record 就是 Avro 里的“对象”或“结构体”。你想想看,任何业务实体——用户、订单、商品——最终都会映射成一个 record。
一个完整的 record 包含三要素:name(名称)、namespace(命名空间,可选但强烈建议)、fields(字段列表)。
{
"type": "record",
"name": "User",
"namespace": "com.example.user",
"fields": [
{ "name": "userId", "type": "string" },
{ "name": "age", "type": "int" },
{ "name": "email", "type": ["null", "string"], "default": null }
]
}
这里有个细节:email 字段的类型是 ["null", "string"],这就是 union 的写法。我建议你给所有可空字段都加上 default 值。为什么?因为 Avro 的 Schema 兼容性检查会用到它。我曾经在线上遇到过因为少写一个 default,导致旧数据反序列化新 Schema 时直接抛异常——那叫一个惨。
我的习惯:所有 record 字段,只要业务上允许为空,就写成 ["null", "实际类型"] 并给 default: null。这能省掉你 80% 的兼容性头疼。
3.2 enum 类型:有限选项的优雅表达
enum 用来表示一组固定的取值。比如订单状态、用户角色、支付方式。Avro 的 enum 比 Java 的 enum 更轻量,它只关心符号名,不关心序号。
{
"type": "enum",
"name": "OrderStatus",
"namespace": "com.example.order",
"symbols": ["PENDING", "PAID", "SHIPPED", "DELIVERED", "CANCELLED"]
}
嗯,这里要注意:symbols 的顺序不要轻易改变。虽然 Avro 的 enum 不依赖 ordinal,但如果你在 Schema 演进时删除了某个 symbol,或者插入了新 symbol 到中间位置,下游消费方可能会收到意料之外的值。我建议你只在末尾追加新 symbol,并且永远不要删除已存在的 symbol——除非你确定所有历史数据都已经被重写了。
避坑指南:我曾经在项目中把 PAID 改成了 PAYMENT_COMPLETED,结果下游的消费者直接反序列化失败。因为 Avro 的 enum 是按符号名匹配的,不是按位置。改名等于删旧加新,属于破坏性变更。
3.3 array 类型:有序列表
array 就是数组,里面每个元素的类型必须一致。写法很简单:
{
"type": "array",
"items": "string"
}
或者嵌套复杂类型:
{
"type": "array",
"items": {
"type": "record",
"name": "LineItem",
"fields": [
{ "name": "productId", "type": "string" },
{ "name": "quantity", "type": "int" },
{ "name": "price", "type": "double" }
]
}
}
array 在 Avro 里没有固定长度限制,也没有“最大元素数”的约束。如果你需要限制,那得在业务层做校验。我个人习惯在 Schema 里用 array 表示“零个或多个”的场景,比如订单的商品明细、日志的标签列表。
3.4 map 类型:键值对容器
map 的键永远是 string,值可以是任意类型。写法:
{
"type": "map",
"values": "long"
}
这个例子表示一个从字符串到长整型的映射。实际业务中,我常用 map 来存“属性扩展”或“动态配置”。比如:
{
"type": "record",
"name": "Product",
"fields": [
{ "name": "productId", "type": "string" },
{ "name": "attributes", "type": { "type": "map", "values": "string" } }
]
}
这样,每个商品可以携带任意数量的自定义属性,而不需要提前定义字段。灵活,但也要小心:map 的键值对是无序的,而且键不能重复。如果你需要有序的键值对,那就用 array 包 record 的方式模拟。
3.5 union 类型:多选一的利器
union 是 Avro 里最灵活也最容易用错的类型。它用 JSON 数组表示,数组里的每个元素都是一个合法的 Avro 类型。序列化时,实际数据只能匹配其中一种。
{
"type": "record",
"name": "Notification",
"fields": [
{
"name": "payload",
"type": ["null", "string", {
"type": "record",
"name": "EmailPayload",
"fields": [
{ "name": "subject", "type": "string" },
{ "name": "body", "type": "string" }
]
}]
}
]
}
这个 payload 字段可以是 null、普通字符串、或者一个 EmailPayload 对象。你想想看,这种设计在消息通知场景下有多方便?一条消息可以携带不同格式的内容。
重要规则:union 里的类型顺序是有意义的。Avro 在序列化时,会按顺序尝试匹配。所以 ["null", "string"] 和 ["string", "null"] 虽然逻辑等价,但序列化后的二进制表示不同。我建议你始终把 null 放在 union 的第一个位置,这是业界惯例。
3.6 综合示例:一个完整的订单 Schema
把上面这些类型串起来,写一个真实的订单 Schema:
{
"type": "record",
"name": "Order",
"namespace": "com.example.order",
"fields": [
{ "name": "orderId", "type": "string" },
{ "name": "userId", "type": "string" },
{ "name": "status", "type": {
"type": "enum",
"name": "OrderStatus",
"symbols": ["PENDING", "PAID", "SHIPPED", "DELIVERED", "CANCELLED"]
}},
{ "name": "items", "type": {
"type": "array",
"items": {
"type": "record",
"name": "LineItem",
"fields": [
{ "name": "productId", "type": "string" },
{ "name": "quantity", "type": "int" },
{ "name": "unitPrice", "type": "double" }
]
}
}},
{ "name": "metadata", "type": ["null", {
"type": "map",
"values": "string"
}], "default": null },
{ "name": "couponCode", "type": ["null", "string"], "default": null }
]
}
这个 Schema 覆盖了 record、enum、array、map、union 五种类型。你可以直接拿去用,或者作为模板修改。
3.7 类型选择决策树
为了帮你快速决策,我画了一张图:
3.8 类型对比速查表
| 类型 | 适用场景 | 键/索引 | 值类型 | 可空性 |
|---|---|---|---|---|
| record | 业务实体、结构化对象 | 字段名(字符串) | 任意类型 | 通过 union 实现 |
| enum | 状态、角色、分类等有限选项 | 符号名(字符串) | 无(自身就是值) | 通过 union 实现 |
| array | 有序列表、批量数据 | 整数索引 | 单一类型 | 通过 union 实现 |
| map | 动态属性、配置项、扩展字段 | 字符串(唯一) | 单一类型 | 通过 union 实现 |
| union | 可空字段、多态数据 | 无(按顺序匹配) | 多种类型之一 | 本身支持 |
我的建议:刚开始写 Avro Schema 时,先画一个简单的类图或 ER 图,再翻译成 Schema。这样不容易漏字段,也能提前发现哪些字段需要 union 或 map。我每次写 Schema 前都会在草稿纸上画五分钟,比直接上手写 JSON 快得多。