数据契约基础:从一次线上事故说起

我记得那是三年前的一个深夜。凌晨两点,我被电话吵醒——线上支付系统挂了。排查下来,原因让人哭笑不得:前端传了一个 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 文档告诉你:怎么调用。数据契约告诉你:数据长什么样。

我画了一张图,帮你理清它们的关系:

数据契约与 API 的关系 数据契约 Schema + 约束 + 版本 定义数据的样子 API 接口 HTTP 方法 + 路径 定义怎么调用 代码实现 Controller + Service 定义怎么处理 核心关系: 数据契约是 API 的"数据说明书",API 是数据契约的"传输通道"。 契约驱动设计 契约驱动测试 契约驱动文档

你看,数据契约是独立的一层。它不关心你是用 REST 还是 gRPC,也不关心你是 GET 还是 POST。它只关心:数据本身对不对。

我习惯这样理解它们的关系:

  • API 是门——告诉你从哪进、怎么进
  • 数据契约是门里的规矩——告诉你进来后该怎么做、不能做什么
  • 代码实现是门里的房间——真正干活的地方

没有数据契约的 API,就像没有规矩的门。谁都可以随便进,进来后想干嘛就干嘛。短期看好像很灵活,长期看就是灾难。

一句话总结:数据契约是 API 设计的"宪法",API 是"法律条文",代码实现是"执法过程"。宪法不变,法律和执法可以灵活调整。

为什么数据契约这么重要

你可能觉得,多写一份文档而已,至于吗?

我跟你讲个真实案例。之前有个项目,两个团队各自开发,约定好接口后就没再同步。三个月后联调,发现:

  • A 团队以为 status 传 "1"、"2"、"3"
  • B 团队以为 status 传 "pending"、"paid"、"cancelled"
  • 两边都觉得自己没错

最后查了半天的聊天记录,才发现当初口头约定的是数字,但文档里写的是字符串。这就是没有数据契约的代价。

有了数据契约,这些问题都能提前发现:

  • Schema 不一致?契约校验直接报错
  • 约束不满足?测试阶段就能暴露
  • 版本不匹配?协商机制自动处理

说白了,数据契约就是把"我以为"变成"我们约定"。它让团队之间的协作从"猜"变成"查"。

我的建议:从第一个接口开始就引入数据契约。不要等到出问题了再补。就像写代码要写注释一样,数据契约应该是 API 设计的标配,而不是选配。