契约设计基础:Schema定义、数据类型约定、版本号管理、兼容性策略
好,咱们进入契约设计的核心部分。说白了,实时特征场景里的契约,就是一套大家必须遵守的“数据规矩”。没有规矩,不成方圆。我在好几个项目里都见过,因为一开始没把契约定清楚,后面联调、上线、排查问题,那叫一个痛苦。
今天咱们就掰开揉碎,把契约设计的四个基础模块讲透:Schema定义、数据类型约定、版本号管理、兼容性策略。这四个东西,是实时特征契约的“四梁八柱”。
Schema定义:把数据长什么样说清楚
Schema,就是数据的“身份证”。它规定了每条数据有哪些字段,每个字段叫什么名字,是什么类型,是必须填还是可选。
我个人习惯,在实时特征场景里,用Avro Schema或者Protobuf来定义。为什么?因为它们自带IDL(接口描述语言),能生成强类型的代码,而且序列化效率高。
举个例子,一个“用户实时点击特征”的Avro Schema长这样:
{
"type": "record",
"name": "UserClickFeature",
"namespace": "com.example.feature",
"fields": [
{"name": "user_id", "type": "string", "doc": "用户唯一标识"},
{"name": "item_id", "type": "string", "doc": "物品ID"},
{"name": "click_time", "type": "long", "doc": "点击时间戳(毫秒)"},
{"name": "click_type", "type": {"type": "enum", "name": "ClickType",
"symbols": ["VIEW", "CLICK", "BUY"]}, "doc": "点击类型"},
{"name": "extra_info", "type": ["null", "string"], "default": null,
"doc": "扩展信息"}
]
}
你看,这里面有几个关键点:
- 字段名:统一用下划线命名法,别一会儿驼峰一会儿蛇形。
- 类型:明确是string、long还是枚举。枚举值要提前定好,别后面加个“UNKNOWN”搞得大家都要改。
- 默认值:对于可选字段,一定要给default。不然下游消费老数据时会报错。
- doc:写清楚这个字段是干嘛的。别偷懒,三个月后你自己都忘了。
数据类型约定:别让类型成为“坑”
数据类型约定,是契约里最容易出问题的地方。我遇到过最离谱的一次,上游把用户ID从string改成了int,下游消费端直接抛异常,整个特征链路断了半小时。
所以,数据类型约定要遵循几个原则:
- 时间戳统一用long:别用string存时间,格式五花八门。统一用毫秒级long,时区统一用UTC。
- ID类字段用string:用户ID、订单ID,哪怕看起来是数字,也用string。因为有些ID可能带字母、带下划线。
- 金额用long或decimal:别用float/double,精度会丢。用long存“分”,或者用decimal。
- 枚举用enum:别用int表示状态,0代表什么、1代表什么,没人记得住。用enum,代码可读性高,IDE还能自动补全。
下面这个表格,是我在项目里常用的数据类型映射:
| 业务含义 | 推荐类型 | 不推荐类型 | 原因 |
|---|---|---|---|
| 用户ID | string | int/long | ID可能带字母或前导0 |
| 时间戳 | long (毫秒) | string | 格式统一,方便计算 |
| 金额 | long (分) | float/double | 避免精度丢失 |
| 状态 | enum | int | 可读性强,不易出错 |
| 扩展字段 | map<string, string> | string (JSON) | 结构化,方便查询 |
版本号管理:给契约一个“身份证号”
版本号管理,是契约演进的“方向盘”。没有版本号,你根本不知道线上跑的是哪个版本的契约。
我建议用语义化版本号:主版本号.次版本号.修订号。
- 主版本号:不兼容的变更。比如删字段、改字段类型。
- 次版本号:向后兼容的新增。比如加一个可选字段。
- 修订号:内部修正,不影响接口。比如改doc描述。
版本号放在哪里?两个地方:
- Schema文件里:作为元数据字段。
- 消息头里:每条消息都带上版本号。这样下游消费时,可以根据版本号做不同的处理。
举个例子,消息头的格式:
{
"schema_version": "2.1.0",
"data": {
"user_id": "u12345",
"click_time": 1700000000000
}
}
这样,下游拿到消息,先看版本号,再决定怎么解析。我曾经在一个项目里,就是因为没在消息头里带版本号,导致新旧数据混在一起,排查了整整两天。
兼容性策略:让新旧版本“和平共处”
兼容性策略,是契约设计的“安”。实时特征场景里,上下游不可能同时升级。所以,你必须定义好:什么样的变更算兼容?什么样的算不兼容?
常见的兼容性策略有三种:
| 策略 | 定义 | 适用场景 | 例子 |
|---|---|---|---|
| 向后兼容 | 新版本可以读旧版本的数据 | 消费者先升级,生产者后升级 | 新增可选字段 |
| 向前兼容 | 旧版本可以读新版本的数据 | 生产者先升级,消费者后升级 | 删除字段(用默认值填充) |
| 完全兼容 | 同时满足向后和向前兼容 | 上下游独立升级,互不影响 | 新增可选字段 + 删除字段有默认值 |
我个人建议,在实时特征场景里,尽量追求完全兼容。因为上下游的升级节奏很难对齐。你想想看,一个特征链路可能涉及十几个服务,每个服务都有自己的发布窗口。如果每次变更都要求上下游同时升级,那项目就别干了。
那如果实在要删字段呢?我的做法是:
- 先把字段标记为废弃(deprecated),但保留在Schema里。
- 通知所有下游,告诉他们这个字段要删了,让他们尽快迁移。
- 等所有下游都确认迁移完了,再在下一个主版本里删掉。
这个过程,我称之为“温柔地删除”。别上来就删,会出人命的。
知识体系总览
下面这张图,把契约设计的基础模块串起来了。你可以把它当作一个“检查清单”,每次设计契约时,对着过一遍。
嗯,到这里,契约设计的基础模块就讲完了。这四个东西,是你在设计任何实时特征契约时,都必须考虑的。别嫌麻烦,前期多花点时间把契约定清楚,后期能省下十倍的时间去排查问题。
我记得有一次,一个同事问我:“为什么你们的特征链路从来没出过兼容性问题?”我说:“因为我们在设计契约时,就把兼容性策略写进了Schema里,而且每次变更都走评审流程。” 说白了,就是把“规矩”定在前面,而不是出了问题再补。