数据契约基础:从一次线上事故说起
我记得那是三年前的一个深夜。凌晨两点,我被电话吵醒——线上支付系统挂了。排查下来,原因让人哭笑不得:前端传了一个 userId 字段,后端接口文档里写的是 user_id。就这一个下划线的差异,整条链路瘫痪了四十分钟。
你可能会说,这不就是字段名没对齐吗?但往深了想,这暴露了一个根本问题:服务之间没有一份双方都认的契约。从那以后,我开始在团队里推行数据契约。今天我们就来聊聊这个基础话题。
什么是数据契约
数据契约,说白了就是服务之间对数据格式、结构和行为的共同约定。它像一份法律合同,明确了谁该提供什么数据、数据长什么样、什么时候该做什么事。
我习惯这样定义:
数据契约 = 接口双方共同认可的数据规范 + 约束条件 + 演进规则
它不只是一份文档,而是贯穿设计、开发、测试、部署全流程的"活约定"。
举个例子。你和另一个团队约定:
- 我给你的订单数据,必须包含
orderId、amount、status三个字段 amount必须是正数,单位是分- 如果我要改字段,必须提前两个版本通知你
这就是一份数据契约。它让双方都知道自己该做什么、不该做什么。
数据契约的三大要素
我拆解过很多数据契约,发现不管多复杂,核心就三个东西:Schema、约束、版本。缺一个,契约就容易变成废纸。
1. Schema:数据的骨架
Schema 定义了数据的结构。它告诉双方:数据里有哪些字段、每个字段是什么类型、是必填还是可选。
我个人习惯用 JSON Schema 来定义。举个例子:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Order",
"type": "object",
"properties": {
"orderId": {
"type": "string",
"description": "订单唯一标识"
},
"amount": {
"type": "integer",
"description": "订单金额,单位:分"
},
"status": {
"type": "string",
"enum": ["pending", "paid", "cancelled"],
"description": "订单状态"
},
"createdAt": {
"type": "string",
"format": "date-time",
"description": "创建时间"
}
},
"required": ["orderId", "amount", "status"]
}
你看,这个 Schema 把数据的样子说得清清楚楚。谁看了都知道该传什么、不该传什么。
我的经验:Schema 定义得越细,后期扯皮越少。尤其是枚举值、格式约束这些,一定要写清楚。我曾经见过一个接口,status 字段传了 "succ"、"success"、"ok" 三种值,就是因为 Schema 里没约束。
2. 约束:数据的边界
光有 Schema 还不够。你得告诉对方:数据在什么范围内才算合法。
约束分几种:
| 约束类型 | 说明 | 示例 |
|---|---|---|
| 值域约束 | 字段的取值范围 | amount > 0,status ∈ {pending, paid, cancelled} |
| 格式约束 | 字段的格式要求 | createdAt 必须是 ISO 8601 格式 |
| 关系约束 | 字段之间的逻辑关系 | paid 状态的订单必须有 paymentTime |
| 业务约束 | 业务规则的限制 | 已取消的订单不能再次支付 |
嗯,这里要注意:约束不是越多越好。我见过一些团队,约束写了几十条,结果接口调用方根本记不住,反而更容易出错。我的建议是:只约束那些违反后会导致系统异常或数据不一致的规则。
3. 版本:数据的演进
系统是会变的。今天三个字段,明天可能变成五个。没有版本管理,数据契约就是一纸空文。
我经历过最惨的一次:上游团队偷偷加了一个字段,下游没适配,结果序列化直接崩了。从那以后,我定了一条铁律:任何数据契约的变更,都必须通过版本号来管理。
版本管理有几个要点:
- 语义化版本:主版本号.次版本号.修订号,比如 v1.2.3
- 向后兼容:新增可选字段算次版本升级,删除或修改必填字段算主版本升级
- 版本协商:调用方和服务方要能协商出双方都支持的版本
- 废弃策略:旧版本要有明确的废弃时间表,不能无限期维护
避坑指南:我曾经见过一个团队,版本号从 v1 直接跳到 v10,中间全是"小改动"。结果没人知道 v5 到 v6 到底改了啥。版本号不是数字游戏,每次升级都要有清晰的 changelog。
数据契约与 API 的关系
很多人把数据契约和 API 文档混为一谈。其实它们不一样。
API 文档告诉你:怎么调用。数据契约告诉你:数据长什么样。
我画了一张图,帮你理清它们的关系:
你看,数据契约是独立的一层。它不关心你是用 REST 还是 gRPC,也不关心你是 GET 还是 POST。它只关心:数据本身对不对。
我习惯这样理解它们的关系:
- API 是门——告诉你从哪进、怎么进
- 数据契约是门里的规矩——告诉你进来后该怎么做、不能做什么
- 代码实现是门里的房间——真正干活的地方
没有数据契约的 API,就像没有规矩的门。谁都可以随便进,进来后想干嘛就干嘛。短期看好像很灵活,长期看就是灾难。
一句话总结:数据契约是 API 设计的"宪法",API 是"法律条文",代码实现是"执法过程"。宪法不变,法律和执法可以灵活调整。
为什么数据契约这么重要
你可能觉得,多写一份文档而已,至于吗?
我跟你讲个真实案例。之前有个项目,两个团队各自开发,约定好接口后就没再同步。三个月后联调,发现:
- A 团队以为
status传 "1"、"2"、"3" - B 团队以为
status传 "pending"、"paid"、"cancelled" - 两边都觉得自己没错
最后查了半天的聊天记录,才发现当初口头约定的是数字,但文档里写的是字符串。这就是没有数据契约的代价。
有了数据契约,这些问题都能提前发现:
- Schema 不一致?契约校验直接报错
- 约束不满足?测试阶段就能暴露
- 版本不匹配?协商机制自动处理
说白了,数据契约就是把"我以为"变成"我们约定"。它让团队之间的协作从"猜"变成"查"。
我的建议:从第一个接口开始就引入数据契约。不要等到出问题了再补。就像写代码要写注释一样,数据契约应该是 API 设计的标配,而不是选配。