4、Avro Schema 基础:Avro 数据类型、Schema 定义、与 JSON 的对比

好,我们进入 Schema 变更自动适配的第一块基石——Avro Schema。

你可能会问:为什么偏偏是 Avro?市面上序列化格式那么多,JSON、Protobuf、Thrift 哪个不能用?

我个人习惯把 Avro 叫做「为 Schema 演化而生的格式」。它从设计之初就把 Schema 和数据的绑定关系放在了第一位。说白了,没有 Schema,Avro 的数据就是一堆二进制乱码;有了 Schema,你才能读懂它、校验它、甚至自动适配它的变化。

4.1 Avro 支持的数据类型

Avro 的数据类型分两大类:基本类型和复杂类型。我刚开始接触时觉得它类型少,用久了才发现——够用,而且精炼。

4.1.1 基本类型

类型 说明 JSON 示例
null 空值,没有数据 null
boolean 布尔值 true / false
int 32 位有符号整数 42
long 64 位有符号整数 1234567890123
float 32 位单精度浮点数 3.14
double 64 位双精度浮点数 3.1415926535
bytes 字节序列 "\u00FF"
string UTF-8 编码字符串 "hello"
一个小坑:我曾经在项目中把时间戳存成 long,后来发现不同服务对毫秒/微秒的理解不一致,导致数据对不上。建议团队统一约定时间戳的精度,或者在 Schema 里用注释写清楚。

4.1.2 复杂类型

复杂类型才是 Avro 真正体现灵活性的地方。我挑几个重点说说。

  • record:最常用的结构,类似 JSON 对象或数据库表行。包含多个命名字段,每个字段有自己的类型。
  • enum:枚举类型,定义一组固定的符号。注意:枚举值一旦发布,不能删除,只能追加。
  • array:数组,所有元素必须是同一类型。
  • map:键值对,键必须是 string,值可以是任意类型。
  • union:联合类型,表示字段可以是多种类型之一。这是 Schema 演化的关键——比如 ["null", "string"] 表示字段可以为 null 或字符串。
  • fixed:固定长度的字节序列,适合存 UUID、哈希值等。

重点记住:union 类型是 Avro 实现向后兼容的「秘密武器」。你想想看,如果新版本给 record 加了一个字段,只要把它声明为 ["null", "新类型"],旧数据就能自动补 null,不会报错。

4.2 Schema 定义:JSON 格式的「数据契约」

Avro Schema 本身是用 JSON 写的。嗯,你没听错——描述数据的 Schema 也是 JSON。这样做的好处是:任何语言都能解析 Schema,不需要额外的解析器。

来看一个完整的例子。假设我们要描述一个用户信息的数据结构:

{
  "type": "record",
  "name": "User",
  "namespace": "com.example.avro",
  "doc": "用户基本信息",
  "fields": [
    {"name": "userId",   "type": "long",   "doc": "用户唯一标识"},
    {"name": "username", "type": "string", "doc": "用户名"},
    {"name": "email",    "type": ["null", "string"], "default": null, "doc": "邮箱"},
    {"name": "age",      "type": "int",    "default": 0, "doc": "年龄"},
    {"name": "tags",     "type": {"type": "array", "items": "string"}, "default": [], "doc": "标签列表"}
  ]
}

这里有几个关键点,我展开说说:

  • type:最外层必须是 "record",表示这是一个复合结构。
  • name:记录的名字,全限定名由 namespace + name 组成。
  • fields:字段列表,每个字段必须有 name 和 type。
  • default:可选,指定字段的默认值。读旧数据时,如果新 Schema 多了字段,就用默认值填充。
  • doc:文档注释,我建议每个字段都写上,团队协作时能省很多沟通成本。
注意:如果字段没有 default,而且类型又不是 union(比如 ["null", "string"]),那么读旧数据时如果缺少该字段,Avro 会直接抛异常。我曾经因为这个原因导致线上消费任务中断了半小时——教训就是:新增字段一定要给默认值,或者用 union 包一层 null。

4.3 Avro Schema vs JSON:为什么不用 JSON 直接干?

这个问题我经常被问到。JSON 多方便啊,人类可读,浏览器原生支持,为什么还要搞一套 Avro?

我画了一张对比图,你看完就明白了。

Avro Schema vs JSON:核心差异 Avro Schema 强类型,Schema 与数据分离 数据体积小(二进制编码) Schema 演化原生支持 新增/删除字段自动兼容 序列化/反序列化速度快 无需解析 JSON 字符串 Schema 注册中心集成 版本管理、兼容性检查 缺点:不直观,调试略麻烦 JSON 弱类型,Schema 隐含在数据中 数据体积大(文本编码) Schema 演化靠人工约定 字段增减容易导致解析失败 序列化/反序列化慢 需要完整解析 JSON 树 无原生 Schema 管理 版本兼容全靠代码控制 优点:人类可读,生态广泛

说白了,JSON 适合「人看数据」,Avro 适合「机器处理数据」。在数据同步场景下,我们追求的是:体积小、速度快、Schema 变化时不出错。这三点 Avro 都做得比 JSON 好。

4.4 实战建议:什么时候用 Avro?

根据我的经验,以下场景优先考虑 Avro:

  • 大数据管道:Kafka、Spark、Flink 等场景,数据量巨大,Avro 的压缩率和性能优势明显。
  • 跨团队数据交换:不同团队维护不同服务,Schema 变更需要自动适配,Avro + Schema Registry 是标配。
  • 长期存储:数据要存几年,Schema 肯定会变。Avro 的演化能力让你不用重写历史数据。

一个小技巧:如果你刚开始用 Avro,建议先用 Avro Tools 把 JSON 数据转成 Avro 格式,体验一下序列化后的体积变化。我记得第一次看到 1GB 的 JSON 转成 Avro 只剩 200MB 时,确实被震撼到了。

好,这一章我们理清了 Avro 的数据类型、Schema 定义方式,以及它和 JSON 的本质区别。下一块内容,我们会深入 Schema 演化的具体规则——哪些变更安全,哪些会炸,以及如何用代码自动检测兼容性。