4. Avro Schema详解:从基础语法到兼容性实战
Avro 的 Schema,说白了就是数据的「说明书」。没有它,二进制数据就是一串乱码。我在做流批一体项目时,吃过不少 Schema 的亏——版本对不上、字段类型不匹配,消费端直接报错。今天咱们就把 Avro Schema 从头捋一遍。
4.1 Avro Schema 基础语法
Avro Schema 用 JSON 描述。嗯,你没看错,就是 JSON。它有两种写法:
- 简单类型:直接写类型名,比如
"string"、"int" - 复杂类型:用 JSON 对象,比如
{"type": "record", "name": "User", ...}
我个人习惯把 Schema 文件存为 .avsc 后缀,方便 IDE 识别。来看一个最简例子:
{
"type": "record",
"name": "User",
"namespace": "com.example",
"fields": [
{"name": "id", "type": "int"},
{"name": "name", "type": "string"}
]
}
这里 namespace 类似 Java 的包名,用来避免重名。我在项目中遇到过两个团队都定义了 Order 类型,结果序列化时串了——加个 namespace 就解决了。
4.2 基本类型与复杂类型
Avro 的基本类型有 8 种,不多,但够用:
| 类型 | 说明 | 示例值 |
|---|---|---|
| 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" |
复杂类型才是重头戏。Avro 支持 6 种:
- record:结构体,最常用
- enum:枚举
- array:数组
- map:字典,key 必须是 string
- union:联合类型,类似「或」
- fixed:固定长度字节
你想想看,union 是不是很像 Java 的 Optional?没错,["null", "string"] 就表示这个字段可以为 null 也可以是字符串。我在做 Kafka 消息时,几乎每个字段都加了 null,避免数据缺失导致反序列化失败。
4.3 Record 与 Enum 的定义
Record 是 Avro 的核心。咱们定义一个复杂点的:
{
"type": "record",
"name": "Order",
"namespace": "com.example.order",
"doc": "订单记录",
"fields": [
{"name": "orderId", "type": "string", "doc": "订单号"},
{"name": "userId", "type": "int"},
{"name": "amount", "type": "double", "default": 0.0},
{"name": "status", "type": {
"type": "enum",
"name": "OrderStatus",
"symbols": ["PENDING", "PAID", "SHIPPED", "CANCELLED"]
}},
{"name": "items", "type": {
"type": "array",
"items": {
"type": "record",
"name": "OrderItem",
"fields": [
{"name": "productId", "type": "int"},
{"name": "quantity", "type": "int", "default": 1}
]
}
}},
{"name": "tags", "type": {"type": "map", "values": "string"}, "default": {}}
]
}
这里有几个要点:
doc字段写注释,生成文档时很有用default给字段设默认值,读旧数据时不会崩- Enum 的
symbols是枚举值列表,顺序不能乱改 - Record 可以嵌套,OrderItem 定义在 Order 内部
4.4 Schema 兼容性规则
这部分是实战中的硬骨头。Avro 的兼容性分 4 种级别:
| 兼容性级别 | 规则 | 典型场景 |
|---|---|---|
| BACKWARD | 新 Schema 能读旧数据 | 消费者升级,生产者未升级 |
| FORWARD | 旧 Schema 能读新数据 | 生产者升级,消费者未升级 |
| FULL | 双向兼容 | 生产者和消费者独立升级 |
| NONE | 不检查兼容性 | 开发测试环境 |
具体规则有哪些?我总结了几条核心的:
- 删除字段:只允许删除有默认值的字段(BACKWARD 下不允许删除)
- 添加字段:必须带默认值(FORWARD 下允许)
- 修改字段类型:只能做兼容性转换,比如 int→long 可以,string→int 不行
- Enum 增减符号:只能在末尾添加,不能删除或重排
- Union 顺序:默认值类型必须排在 union 的第一个位置
为什么会这样?Avro 序列化时,Enum 存的是索引(0, 1, 2...),不是字符串。你插在中间,索引全乱了。同理,删除符号也会导致索引错位。
4.5 兼容性实战:一个 Schema 演进案例
假设我们有个 User 表,v1 版本:
{
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "int"},
{"name": "name", "type": "string"}
]
}
现在要加一个 email 字段,同时废弃 phone 字段。怎么做才兼容?
我的做法是分两步走:
- v2 版本:添加 email(带默认值),phone 保留但标记 deprecated
- v3 版本:等所有消费者都升级后,再删除 phone
// v2 版本
{
"type": "record",
"name": "User",
"fields": [
{"name": "id", "type": "int"},
{"name": "name", "type": "string"},
{"name": "email", "type": ["null", "string"], "default": null},
{"name": "phone", "type": ["null", "string"], "default": null, "doc": "deprecated"}
]
}
核心原则:Schema 演进要遵循「加字段不加索引,删字段不删定义」的策略。用 union + null 做可选字段,用 doc 标记废弃字段。
4.6 知识体系总览
下面这张图把 Avro Schema 的核心知识点串起来了:
这张图把 Avro Schema 的四个核心维度串在一起了。你从中心出发,往左是基本类型,往右是复杂类型,往下是兼容性规则。每个分支都有对应的子节点,方便你快速定位知识点。
4.7 避坑总结
最后,我把自己在项目中踩过的坑列出来,你直接拿去用:
- Enum 不要改顺序:索引编码,改了就是灾难
- Union 默认值放第一位:否则反序列化会选错类型
- 删除字段要分两步:先标记 deprecated,等所有消费者升级后再删
- Namespace 一定要加:避免不同团队的类型冲突
- Schema Registry 要开启兼容性检查:别用 NONE,除非你在玩火