为什么需要一份模板
卷二反复出现同一类争论:这个字段谁能写、重复回调怎么办、下游挂了谁负责、数据保留多久。争论本身不可怕,可怕的是每次都要从头吵一遍,最后靠会议纪要里的模糊句子收场。
契约模板的作用,是把这些争论变成固定栏目。填不满的栏目,就是尚未想清楚的地方;填出来互相矛盾的栏目,就是评审要先解决的问题。
模板不追求覆盖所有系统。它只要求每个核心系统在接入其他系统之前,至少能回答下面这些问题。
一、契约卡的最小字段
| 字段 | 要回答的问题 | 不合格表现 |
|---|---|---|
| 系统名与负责人 | 谁拥有这个系统,出问题找谁 | 只写团队名,没有值班负责人 |
| 拥有的数据 | 哪些数据由本系统唯一写入 | “用户数据”这种模糊集合 |
| 写入者 | 除本系统外,谁还能写;为什么 | 多个服务都可写同一张表 |
| 读取者 | 谁依赖这些数据,以什么方式 | 读取方直接连库 |
| 幂等键 | 写操作的唯一标识由谁生成 | “重复请求忽略”但没说怎么识别 |
| 一致性 | 强一致还是最终一致,窗口多大 | 所有字段都写“强一致” |
| 失败语义 | 超时、重复、部分成功分别返回什么 | 只定义成功返回 |
| SLA | 可用性、延迟、恢复时间 | 只有“高可用”三个字 |
| 保留期与删除 | 数据留多久,删除请求如何传导 | 不写删除责任 |
| 降级 | 依赖不可用时本系统怎么退化 | 假设下游永远可用 |
一张契约卡通常一页到两页。超过两页,说明边界没有切干净。
二、填写顺序
建议按下面的顺序填,因为后面的栏目依赖前面的结论。
第一步:先定数据所有权
列出本系统拥有的实体和关键字段,逐项确认“唯一写入者”。如果一个字段需要两个系统写,先决定谁拥有,另一个改为通过接口或事件请求。
判据:能否用一个唯一约束或写入接口把“第二个写入者”挡住。挡不住,所有权就只是文档上的声明。
第二步:再定读写路径
- 外部系统只能通过接口或事件读取,不允许直连数据库;
- 读取方需要的是原始数据还是聚合结果,决定提供查询接口还是订阅事件;
- 跨系统查询要明确一致性窗口,不要假设“刚写完就能读到”。
第三步:定义写操作的幂等语义
每个写操作回答三个问题:
- 幂等键是什么,由调用方还是服务方生成;
- 重复请求返回什么:原结果、冲突错误还是静默成功;
- 幂等记录保留多久,过期后重复请求会怎样。
第四步:写失败语义
至少覆盖五类失败:
| 失败 | 返回 | 调用方应该做什么 |
|---|---|---|
| 参数错误 | 明确错误码 | 修正后重试 |
| 未授权/越权 | 拒绝,不泄露内部信息 | 不要重试 |
| 资源冲突 | 冲突码 + 当前状态 | 读取后决定 |
| 暂时不可用 | 可重试错误 + 建议退避 | 退避重试 |
| 结果未知(超时) | 查询接口或幂等键 | 用同一幂等键重试 |
“结果未知”最容易被忽略。调用方超时后既不能当成功,也不能当失败,必须有一个查询或幂等机制让它确认。
第五步:定 SLA 与降级
SLA 要分接口、分场景,不要给整个系统一个笼统数字。降级要写清楚:
- 依赖不可用时,本系统返回缓存、默认值还是错误;
- 降级期间哪些写操作被拒绝,恢复后如何补偿;
- 谁有权触发降级,多久复审一次。
三、四个填写示例
3.1 账号服务
| 字段 | 内容 |
|---|---|
| 拥有的数据 | 账号主体、外部身份映射、设备绑定、注销状态 |
| 唯一写入者 | 账号服务;游戏服只能读,不能改账号状态 |
| 幂等键 | 绑定请求使用 provider + external_id + tenant |
| 一致性 | 账号创建强一致;跨游戏资料最终一致,窗口秒级 |
| 失败语义 | 绑定冲突返回冲突码;注销中账号拒绝新绑定 |
| SLA | 登录校验 P99 50ms;绑定 P99 300ms(示例值) |
| 保留期 | 账号主体保留至注销完成;审计日志按合规期限 |
| 降级 | 账号库不可用时,登录校验走短期票据缓存,禁止新注册 |
关键点:账号状态只有一个写入者。游戏服可以缓存昵称和等级,但不能改账号的封禁、注销与绑定关系。
3.2 结算服务
| 字段 | 内容 |
|---|---|
| 拥有的数据 | 结算单、发奖记录、冲正记录 |
| 唯一写入者 | 结算服务;战斗服只提交结算请求 |
| 幂等键 | battle_id + season_id(或业务定义的对局唯一键) |
| 一致性 | 结算单与发奖记录同事务;跨游戏资产最终一致 |
| 失败语义 | 重复请求返回原结算单;发奖失败保留待重试状态 |
| SLA | 结算请求受理 P99 100ms;发奖完成 P99 2s(示例值) |
| 保留期 | 结算单长期保留;原始战斗日志按成本分级 |
| 降级 | 发奖通道不可用时进入待发队列,不丢结算单 |
关键点:先落结算单,再发奖。顺序反过来,一旦发奖后崩溃,就无法判断是否已发。
3.3 支付服务
| 字段 | 内容 |
|---|---|
| 拥有的数据 | 支付订单、渠道回调记录、退款单、对账差异 |
| 唯一写入者 | 支付服务;游戏服只能查询订单状态 |
| 幂等键 | 内部订单号;渠道回调按渠道单号去重 |
| 一致性 | 订单状态强一致;发货通知最终一致,窗口分钟级 |
| 失败语义 | 验签失败拒绝;重复回调返回已处理;金额不符挂起 |
| SLA | 回调处理 P99 500ms;对账日终完成(示例值) |
| 保留期 | 订单与退款按财务与合规要求保留 |
| 降级 | 渠道不可用时停止新下单,不停止已支付订单的发货 |
关键点:支付状态与发货状态分开。已支付未发货是合法中间态,必须有补偿任务,而不是靠人工发现。
3.4 平台能力
| 字段 | 内容 |
|---|---|
| 拥有的数据 | 能力注册表、租户配置、配额与 SLA 指标 |
| 唯一写入者 | 平台服务;游戏侧只能申请配额与读取指标 |
| 幂等键 | 能力开通请求使用 game_id + capability + version |
| 一致性 | 配置变更最终一致,窗口秒级;配额强一致 |
| 失败语义 | 未开通能力返回明确错误;越权访问拒绝并审计 |
| SLA | 按能力分别定义,不设统一数字 |
| 保留期 | 审计日志长期保留;指标按成本滚动 |
| 降级 | 单一游戏超配额时只限该租户,不影响其他游戏 |
关键点:SLA 按能力定义。账号可用性 99.95% 和推荐服务可用性 99.9% 不是同一件事,混在一个数字里无法追责。
四、评审时的十个问题
拿到一张契约卡,按顺序问:
- 这份数据如果出现两个写入者,哪一个约束会挡住第二个?
- 读取方是走接口还是直连库?直连的什么时候迁走?
- 幂等键由谁生成?重复请求返回什么?
- 超时后调用方如何确认结果?
- 部分成功时,已成功的部分如何回滚或补偿?
- 最终一致的窗口多大?窗口内读到旧值会怎样?
- SLA 是按接口还是按系统?故障时如何统计?
- 删除请求如何传导到缓存、日志和备份?
- 依赖不可用时,降级返回什么?谁有权触发?
- 上一次契约变更是什么时候?旧版本还支持多久?
十个问题里有两个答不上来,契约就不应该签字。
五、常见错误
| 错误 | 后果 | 纠正 |
|---|---|---|
| 把接口文档当契约 | 只写了成功路径 | 补失败语义、幂等和 SLA |
| 所有权写成“共同维护” | 出事无人负责 | 指定唯一写入者 |
| 幂等靠“客户端不会重试” | 网络超时必然重试 | 服务端强制幂等键 |
| 所有数据都强一致 | 成本高、跨区不可行 | 按字段定一致性 |
| SLA 只有一个总数 | 无法定位与追责 | 按接口/能力拆分 |
| 降级没有触发条件 | 出事时不敢用或乱用 | 写清阈值与授权人 |
| 删除只删主表 | 合规风险 | 删除任务覆盖缓存、日志、备份 |
| 契约从不复审 | 实现早已偏离 | 变更留版本与复审日期 |
六、一页纸模板
系统:____ 负责人/值班:____
拥有的数据:____
唯一写入者:____ 其他写入者及理由:____
读取方与方式:____
幂等键:____ 重复请求返回:____
一致性:强一致 ____ / 最终一致 ____(窗口 ____)
失败语义:参数 ____ / 越权 ____ / 冲突 ____ / 不可用 ____ / 结果未知 ____
SLA:接口 ____ P99 ____;可用性 ____;恢复时间 ____
保留期:____ 删除传导:____
降级:触发条件 ____;返回 ____;授权人 ____;复审 ____
版本:____ 上次复审:____
模板本身不解决问题,它只是让“没想清楚”无处可藏。真正有价值的契约,是两个人分别填写后能得到同一份答案;如果填出来不一样,说明边界还需要再谈一次。
契约卡填完之后,建议和附录 C 的故障模式清单一起过一遍:每个失败语义,是否都有对应的可探测信号和兜底动作。两页纸对不上,系统就还没有准备好接入别人。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。