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 编码的文本字符串
我的习惯:能用 int 就别用 long,除非你确定数值会超过 20 亿。序列化时 int 占 4 字节,long 占 8 字节,积少成多,对存储和网络开销都有影响。

3.2 复杂类型:Complex Types

基础类型只能表达简单字段。真实业务里,我们需要的是嵌套结构、枚举、集合和可选值。Avro 提供了 6 种复杂类型:Record、Enum、Array、Map、Union 和 Fixed。

下面这张图帮你快速建立整体认知:

Avro 复杂类型体系 Avro Schema Record Enum Array Map Union Fixed Record 和 Union 是 Schema Evolution 中最关键的两个类型 Enum、Array、Map 的变更规则相对严格,需要提前规划

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 真的会抓狂。
我曾经踩过的坑:给 Record 的 name 用了中文。Avro 规范里 name 必须是合法的标识符(字母、数字、下划线),中文在某些序列化器里会直接报错。老老实实用英文吧。

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 或字符串,灵活性很高。

我的建议:Map 虽然灵活,但别滥用。如果字段是固定的,用 Record 更清晰。Map 适合那些你无法预知 key 的场景,比如标签系统、自定义属性。

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 最灵活,但别滥用。