3. Avro Schema 详解:数据类型、Record、Enum、Array、Map、Union 的定义与使用
Avro Schema 是 Avro 序列化框架的基石。说白了,它就是一份描述数据结构的「契约」。我当年刚接触 Avro 时,觉得它跟 JSON Schema 差不多,后来踩了几个坑才发现——细节里藏着魔鬼。
这一节,我们就把 Avro 的几种核心数据类型掰开揉碎。从基础类型到复杂结构,再到最让人头疼的 Union,一个一个过。
3.1 基础数据类型:Primitive Types
Avro 定义了一组基础类型,用来表示最简单的数据单元。这些类型在 JSON 形式的 Schema 里直接用字符串表示。
| 类型名 | JSON 表示 | 说明 |
|---|---|---|
| null | "null" |
空值,没有具体数据 |
| boolean | "boolean" |
布尔值,true 或 false |
| int | "int" |
32 位有符号整数 |
| long | "long" |
64 位有符号整数 |
| float | "float" |
单精度浮点数(32 位) |
| double | "double" |
双精度浮点数(64 位) |
| bytes | "bytes" |
二进制字节序列 |
| string | "string" |
UTF-8 编码的文本字符串 |
3.2 复杂类型:Complex Types
基础类型只能表达简单字段。真实业务里,我们需要的是嵌套结构、枚举、集合和可选值。Avro 提供了 6 种复杂类型:Record、Enum、Array、Map、Union 和 Fixed。
下面这张图帮你快速建立整体认知:
3.3 Record:最常用的结构体
Record 相当于编程语言里的结构体或类。它由一组命名字段组成,每个字段都有自己的类型和默认值。
一个典型的 Record 定义长这样:
{
"type": "record",
"name": "User",
"namespace": "com.example",
"doc": "用户基本信息",
"fields": [
{"name": "id", "type": "long", "doc": "用户唯一标识"},
{"name": "name", "type": "string"},
{"name": "age", "type": "int", "default": 0},
{"name": "email", "type": ["null", "string"], "default": null}
]
}
这里有几个关键点:
- name:字段名,必须唯一。我建议用驼峰命名,跟 Java/Python 风格保持一致。
- type:字段类型,可以是基础类型,也可以是复杂类型。
- default:可选。如果读数据时发现字段缺失,就用默认值填充。这是 Schema Evolution 的核心机制之一。
- doc:文档说明,强烈建议写上。你想想看,半年后你回来看自己的 Schema,没有 doc 真的会抓狂。
3.4 Enum:枚举类型
Enum 定义了一组固定的符号常量。它的 Schema 结构很简单:
{
"type": "enum",
"name": "UserStatus",
"namespace": "com.example.enums",
"symbols": ["ACTIVE", "INACTIVE", "BANNED"]
}
Enum 的进化规则非常严格:
- 可以新增 symbol,但只能追加到末尾。
- 不能删除 symbol,否则旧数据反序列化时会直接崩溃。
- 不能修改 symbol 名称,比如把 "ACTIVE" 改成 "ACTIVATED",旧数据就认不出来了。
我在项目中遇到过一个问题:业务方想废弃 "BANNED" 状态,改成 "SUSPENDED"。按 Avro 的规则,不能直接改。最后我们只能新增 "SUSPENDED",然后在应用层做兼容处理。嗯,这算是个教训——Enum 的 symbol 设计一定要考虑长远。
3.5 Array:数组
Array 表示一组同类型元素的集合。定义方式很简单:
{
"type": "array",
"items": "string"
}
也可以嵌套复杂类型:
{
"type": "array",
"items": {
"type": "record",
"name": "Address",
"fields": [
{"name": "city", "type": "string"},
{"name": "street", "type": "string"}
]
}
}
Array 的进化相对宽松。你可以修改 items 的类型,但必须保证新旧类型兼容。比如从 int 改成 long 是安全的,但从 string 改成 int 就会出问题。
3.6 Map:键值对集合
Map 的键固定为 string 类型,值可以是任意类型:
{
"type": "map",
"values": "double"
}
实际业务里,Map 常用于存储动态属性。比如用户的扩展字段:
{
"type": "map",
"values": ["null", "string"]
}
这样每个 value 可以是 null 或字符串,灵活性很高。
3.7 Union:联合类型(最核心也最复杂)
Union 表示「这个字段可以是多种类型中的一种」。它的语法是用 JSON 数组列出所有可能的类型:
["null", "string", "int"]
Union 最常见的用途是实现可选字段。比如:
{"name": "phone", "type": ["null", "string"], "default": null}
这意味着 phone 字段可以是 null 或 string。如果写入时没提供,就默认为 null。
Union 的规则有几个硬性约束:
- 不能包含两个同种类型,比如
["string", "string"]是非法的。 - null 通常放在第一位,这是 Avro 的约定俗成。
- Record 类型在 Union 中必须有 name,否则无法区分。
Union 的进化规则比较灵活:
- 可以新增类型,但必须保证旧数据能匹配到原有类型。
- 可以删除类型,但前提是旧数据里没有使用该类型的实例。
- 调整类型的顺序是安全的,因为 Avro 通过索引来匹配。
核心要点:Union 是 Schema Evolution 中最强大的工具。它让你可以在不破坏兼容性的前提下,逐步扩展字段的可能性。但也要注意,Union 会增加序列化开销,因为每个值都需要额外存储一个类型索引。
3.8 Fixed:固定长度字节
Fixed 用于表示固定长度的二进制数据。比如 UUID、哈希值:
{
"type": "fixed",
"name": "UUID",
"size": 16
}
Fixed 的 size 一旦定义就不能改。改了就是全新的类型,新旧完全不兼容。所以用 Fixed 之前,一定要确认长度不会变。
3.9 实战:一个完整的 Schema 示例
把上面这些类型组合起来,就是一个真实的业务 Schema:
{
"type": "record",
"name": "Order",
"namespace": "com.example.order",
"fields": [
{"name": "orderId", "type": "string"},
{"name": "userId", "type": "long"},
{"name": "status", "type": {
"type": "enum",
"name": "OrderStatus",
"symbols": ["PENDING", "PAID", "SHIPPED", "COMPLETED", "CANCELLED"]
}},
{"name": "items", "type": {
"type": "array",
"items": {
"type": "record",
"name": "OrderItem",
"fields": [
{"name": "productId", "type": "long"},
{"name": "quantity", "type": "int"},
{"name": "price", "type": "double"}
]
}
}},
{"name": "tags", "type": ["null", {
"type": "map",
"values": "string"
}], "default": null},
{"name": "couponCode", "type": ["null", "string"], "default": null}
]
}
这个 Schema 涵盖了 Record、Enum、Array、Map、Union 五种复杂类型。你可以看到:
- Order 本身是 Record。
- status 是 Enum,只有 5 种固定状态。
- items 是 Array,里面每个元素又是一个 Record。
- tags 是 Union,可以是 null 或 Map。
- couponCode 是 Union,可以是 null 或 string。
嗯,这就是一个典型的电商订单结构。我在实际项目中,类似的 Schema 要维护几十个。每次版本升级,都要仔细检查兼容性。
3.10 小结
Avro 的数据类型体系,说复杂也复杂,说简单也简单。核心就记住几条:
- 基础类型是砖瓦,复杂类型是结构。
- Record 是骨架,Union 是关节。
- Enum 和 Fixed 最「死板」,定义前想清楚。
- Array 和 Map 最灵活,但别滥用。