Schema 基础:到底什么是 Schema?核心概念与术语解析
说实话,我入行头两年,一直觉得 Schema 是个挺玄乎的词。那时候带我的老架构师跟我说:“你写 DDL 就是在定义 Schema。” 我心想,不就是建表嘛,搞这么高大上干嘛?
后来踩的坑多了,才明白 Schema 远不止几张表那么简单。它其实是数据的“宪法”——规定了数据长什么样、能怎么用、彼此之间什么关系。没有它,数据就是一团乱麻。
从一张图说起:Schema 在数据体系中的位置
先看下面这张图,这是我给团队做培训时必讲的一张结构图。它把 Schema 放在数据治理的中间层,承上启下。
你看,Schema 卡在业务和存储中间。它既不是纯业务的东西,也不是纯技术的东西。我见过不少团队,业务方直接写 SQL 建表,字段名用拼音缩写,类型随便选——结果半年后谁也看不懂那张表在存什么。这就是典型的“跳过 Schema 治理”的后果。
Schema 的核心概念:别被术语吓到
咱们把 Schema 拆开揉碎了看。其实核心就这几个概念,搞懂了它们,后面所有治理手段都是围绕它们展开的。
1. 字段(Field / Column)
这是最基础的单位。一个字段包含三样东西:名字、类型、语义。
- 名字:比如
user_id、created_at。我建议团队统一用蛇形命名法(snake_case),别混用驼峰。 - 类型:
INT、STRING、TIMESTAMP这些。类型选错会出大问题——我见过有人用STRING存金额,结果排序时按字典序排,100 比 20 小,财务对账对了一周才发现。 - 语义:这个字段到底代表什么?是“用户注册时间”还是“最后登录时间”?语义不清是数据血缘混乱的根源。
comment。哪怕你觉得“这字段名一看就懂”,也写一句注释。半年后你再看,感谢自己。
2. 记录(Record / Row)
一组字段的集合,就是一条记录。在关系型数据库里叫“行”,在 Avro 里叫“record”,在 Protobuf 里叫“message”。名字不同,本质一样——都是把多个字段打包成一个逻辑单元。
3. 类型系统(Type System)
这是 Schema 里最容易出幺蛾子的地方。不同的数据格式,类型系统差别很大:
| 格式 | 基本类型 | 复杂类型 | 空值处理 |
|---|---|---|---|
| Avro | null, boolean, int, long, float, double, bytes, string | record, enum, array, map, union, fixed | union[null, type] |
| Protobuf | double, float, int32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string, bytes | message, enum, oneof, map | optional / 默认值 |
| Thrift | bool, byte, i16, i32, i64, double, string, binary | struct, union, enum, list, set, map | optional / required |
| JSON Schema | string, number, integer, boolean, null | object, array | nullable: true |
你看,光是“空值”怎么表示,各家都不一样。Avro 用 union[null, string],Protobuf 用 optional,Thrift 用 optional 关键字。如果你在异构系统之间传数据,类型映射没做好,空值就会变成 "" 或者 0,数据质量直接崩掉。
union[null, int] 改成了 int,结果所有 null 值被反序列化成 0。下游的告警系统以为流量跌零了,半夜把值班的人全叫起来。嗯,从那以后我改 Schema 都先跑兼容性检查。
4. 命名空间(Namespace)
说白了就是“分组”。在 Avro 里叫 namespace,在 Protobuf 里叫 package,在 Java 里叫 package。它的作用是避免名字冲突。
举个例子,订单系统和用户系统都可能有一个 Event 类型。没有命名空间,你只能叫 OrderEvent 和 UserEvent。有了命名空间,com.order.Event 和 com.user.Event 就能和平共处。
5. 版本(Version)
Schema 是会变的。今天加个字段,明天改个类型,后天废弃一个字段。没有版本管理,你根本不知道下游消费的是哪个版本的 Schema。
我见过最离谱的事:一个团队在 Kafka 里发了新版本的 Avro Schema,但没更新 Schema Registry 里的兼容性配置。结果老消费者反序列化失败,整个数据管道堵了三个小时。原因?新版本加了一个 required 字段,老数据里没有这个字段。
Schema 的三种形态:逻辑、物理、交换
你可能听过“逻辑 Schema”和“物理 Schema”这两个词。我习惯再加一个“交换 Schema”,因为在实际工作中,这三个层面经常打架。
- 逻辑 Schema:业务视角的数据结构。比如“用户信息包含姓名、手机号、注册时间”。它不关心底层怎么存。
- 物理 Schema:存储引擎视角的数据结构。比如 MySQL 里的表结构、HBase 里的列族设计、Parquet 里的列式存储格式。
- 交换 Schema:数据传输时的序列化格式。比如 Kafka 消息里的 Avro Schema、API 接口里的 JSON Schema。
这三者之间经常有“阻抗不匹配”。举个例子:逻辑上“手机号”是一个字段,物理上你可能拆成“国家码”和“号码”两列,交换时又合并成一个字符串。这种映射关系如果不文档化,后面的人根本看不懂。
Schema 的常见格式:选型指南
现在主流的 Schema 格式就几种,我按使用场景给你排个序:
| 格式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Avro | Kafka 消息、Hadoop 生态 | 自带 Schema 演进、压缩率高、支持复杂类型 | JSON 格式的 Schema 文件略啰嗦 |
| Protobuf | 微服务 RPC、高性能场景 | 序列化快、体积小、代码生成强 | Schema 文件可读性差、版本演进限制多 |
| Thrift | 跨语言服务、Facebook 生态 | 支持多种传输协议、IDL 清晰 | 社区活跃度不如 Protobuf |
| JSON Schema | REST API、配置文件 | 人类可读、生态广泛、校验规则丰富 | 无二进制编码、体积大、无内置演进机制 |
我个人习惯:数据管道用 Avro,服务间调用用 Protobuf,API 文档用 JSON Schema。这不是的,但这样分工比较清晰。
Schema Registry:为什么你需要一个“中央注册中心”?
有了 Schema 之后,下一个问题就是:Schema 放哪儿?
你想想看,如果每个生产者都自己定义 Schema,消费者怎么知道该用哪个版本?靠口头沟通?靠邮件发文件?
所以需要一个中央注册中心——Schema Registry。它的核心职责就三件事:
- 存储:所有 Schema 的历史版本都存下来,不能丢。
- 校验:新注册的 Schema 必须通过兼容性检查,不能随便改。
- 分发:消费者通过 Schema ID 就能拿到对应的 Schema,不用自己猜。
Confluent Schema Registry 是业界最成熟的方案,支持 Avro、Protobuf、JSON Schema。它内置了多种兼容性策略:
| 策略 | 规则 | 适用场景 |
|---|---|---|
| BACKWARD | 新 Schema 可以读老数据 | 消费者先升级,生产者后升级 |
| FORWARD | 老 Schema 可以读新数据 | 生产者先升级,消费者后升级 |
| FULL | 双向兼容 | 生产者和消费者同时升级 |
| NONE | 不检查兼容性 | 开发环境、快速迭代 |
NONE。我见过一个团队为了省事,把 Schema Registry 的兼容性检查关了,结果上线后下游反序列化全部报错,回滚花了两个小时。省事一时爽,回滚火葬场。
小结:Schema 到底是什么?
说了这么多,回到最初的问题:Schema 到底是什么?
我的答案是:Schema 是数据的“契约”。它约定了数据的形状、含义和演变规则。没有这个契约,数据生产者想怎么改就怎么改,消费者只能被动接盘——这就是混乱的根源。
而 Schema 治理,就是把这个契约从“口头约定”变成“系统强制执行”的过程。后面我们会一步步展开,讲怎么定规范、怎么落地、怎么应对各种奇葩场景。