3. Schema 定义与版本化:Avro、Protobuf、JSON Schema 简介,Schema 版本号管理策略(语义化版本)
好,我们进入第三章。这一章聊的是数据契约的“骨架”——Schema。说白了,就是定义你的数据长什么样,有哪些字段,每个字段是什么类型。没有 Schema,数据就是一盘散沙,上下游根本没法对齐。
但光有 Schema 还不够。业务在变,字段在加,类型在改,你怎么保证改了之后老系统不崩?这就引出了版本化。我见过太多团队,Schema 改了直接覆盖,下游消费端悄无声息地挂掉,然后排查半天才发现是字段对不上了。嗯,咱们今天就把这事彻底理清楚。
3.1 三种主流 Schema 语言:Avro、Protobuf、JSON Schema
目前业界用得最多的就是这三种。它们各有各的脾气,选型时得看你的场景。
| 特性 | Avro | Protobuf | JSON Schema |
|---|---|---|---|
| 序列化格式 | 二进制(紧凑) | 二进制(紧凑) | JSON(文本,可读性强) |
| Schema 存储 | 通常与数据一起存储或注册中心 | 通常预编译进代码 | 独立文件,运行时解析 |
| Schema 演化 | 强支持(字段可增删改,有默认值机制) | 支持(有编号和规则,但删除字段需谨慎) | 支持(通过 allOf/oneOf 等组合,但较复杂) |
| 性能 | 高(适合大数据批量场景) | 高(适合 RPC 和微服务) | 中(解析开销大,适合配置类数据) |
| 典型场景 | Kafka 消息、Hadoop 生态 | gRPC、微服务间通信 | API 文档、配置文件、轻量级校验 |
3.2 Avro 快速上手
Avro 的 Schema 定义用 JSON 写。举个例子,一个用户信息的数据契约:
{
"type": "record",
"name": "User",
"namespace": "com.example.contract",
"fields": [
{ "name": "userId", "type": "string" },
{ "name": "userName", "type": "string" },
{ "name": "email", "type": ["null", "string"], "default": null },
{ "name": "age", "type": "int", "default": 0 }
]
}
注意看 email 字段,我用了联合类型 ["null", "string"],并且给了默认值 null。这是 Avro 演化友好的关键——新加的字段只要有默认值,老数据反序列化时就不会报错。
3.3 Protobuf 快速上手
Protobuf 的 Schema 定义在 .proto 文件中。同样的用户信息:
syntax = "proto3";
package com.example.contract;
message User {
string user_id = 1;
string user_name = 2;
optional string email = 3;
int32 age = 4;
}
Protobuf 的每个字段都有一个唯一的编号(= 1、= 2 等)。这个编号一旦发布,就永远不能改。为什么?因为序列化时用的是编号而不是字段名。你改了编号,老数据就全乱了。
另外,proto3 中所有字段都是 optional 的,但如果你用 optional 关键字显式声明,可以区分“字段没设置”和“字段设了默认值”。我个人建议显式声明,语义更清晰。
3.4 JSON Schema 快速上手
JSON Schema 是纯文本的,可读性最好。同样的用户信息:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "User",
"type": "object",
"properties": {
"userId": { "type": "string" },
"userName": { "type": "string" },
"email": { "type": "string" },
"age": { "type": "integer", "minimum": 0 }
},
"required": ["userId", "userName"]
}
JSON Schema 的演化主要靠 allOf、oneOf 等组合关键字。比如你要加一个地址字段,可以定义一个新的 Schema,然后用 allOf 合并。但说实话,JSON Schema 的版本管理不如 Avro 和 Protobuf 那么原生,更多是靠外部工具和约定。
3.5 Schema 版本号管理策略:语义化版本
有了 Schema,怎么管版本?我强烈推荐语义化版本(SemVer)。格式很简单:MAJOR.MINOR.PATCH。
- MAJOR(主版本):不兼容的变更。比如删除了一个字段,或者改了字段类型。下游必须改造才能适配。
- MINOR(次版本):向后兼容的新增。比如加了一个可选字段,或者加了一个有默认值的字段。下游不需要改代码。
- PATCH(补丁版本):向后兼容的修复。比如修正了字段描述,或者调整了校验规则。不影响数据格式。
举个例子,你原来有个 User 契约,版本 1.0.0。现在你要加一个 phoneNumber 字段,并且它是可选的(有默认值),那版本号应该升到 1.1.0。如果你把 userId 的类型从 string 改成 int,那就是 2.0.0。
你想想看,如果没有这个规则,下游看到版本号从 1.0.0 变成 1.0.1,以为只是小修小补,结果一接数据发现字段类型变了,那不就炸了吗?
3.6 知识体系总览
下面这张图把本章的核心逻辑串起来了。你可以看到,从 Schema 定义到版本化,再到兼容性检查,是一条完整的链路。
3.7 版本号管理的最佳实践
光有理论不够,落地才是关键。我总结了几条实战经验:
- 版本号必须存储在 Schema 元数据中。无论是 Avro 的
doc字段,还是 Protobuf 的option,或者 JSON Schema 的$id,都要把版本号写进去。这样数据流到哪里,版本号就跟到哪里。 - 使用 Schema Registry 做兼容性检查。Confluent Schema Registry 支持 Avro 和 Protobuf,可以配置兼容性级别(BACKWARD、FORWARD、FULL 等)。每次注册新版本时,它会自动检查是否兼容。不兼容就拒绝注册。
- 版本号变更必须走审批流程。MAJOR 变更需要架构师审批,MINOR 变更需要团队负责人审批,PATCH 变更可以自动通过。这个流程我会在后面的章节详细讲。
- 保留所有历史版本。不要只保留最新版本。老版本可能还有数据在流转,你需要能回溯。Schema Registry 天然支持版本历史。
doc 字段里写 "v1.1.0: add phoneNumber field for SMS notification"。这样以后排查问题,一眼就能看出每个版本改了啥。