Skip to content

Event Contract(事件契约) ​

解决什么问题:异步跨服务调用没有"请求-响应"那一握手,调用方凭什么相信生产者的格式承诺—— Event Contract = 事件的"宪法",告诉所有订阅者"这个事件永远长这样、版本怎么演进、我下架了怎么通知你"。

它是什么 ​

Event Contract(事件契约) 是 L2 设计层关于异步事件格式的强约束契约——它告诉所有订阅者:

  1. 结构:字段名、类型、是否必填、是否嵌套
  2. 语义:事件代表什么"已发生的事实",不可修改只能追加
  3. 版本:怎么标版本、新版本何时算 breaking
  4. 演化规则:加字段 OK、改字段危险、删字段要带默认
  5. 下架流程:deprecation 公告、宽限期、强制截止日

EDM Ch 3(Bellemare): "An event contract is the formal agreement between producers and consumers about the structure, semantics, and lifecycle of events. Without a contract, every consumer becomes a point of fragile coupling."

Event Contract ≠ Schema。Schema 是结构定义(机器可读),Contract 是"结构 + 语义 + 演化 + 下架"的完整协议。

V2 视角(V2.7 B7 段 5 "自然演化" + B2 段 4 "Hyrum's Law"):事件是"已发生的业务事实"——事实不可撤销,所以事件一旦发布就是历史,契约的演化必须保证历史可解读性。

关键属性 ​

属性值/含义
不可变性已发布事件的事实不可修改,只能追加新事件
向后兼容加字段、加可选字段永远兼容
向前兼容新版消费者能读老事件,老版消费者读新事件要优雅降级
版本化Schema 版本(整数或 semver)+ 文档版本独立
语义锁定字段名定了 = 业务含义定了,不能改语义不改名
下架可见deprecation 必须公告 + 监控 + 宽限期
反模式直接改 schema、删字段不带默认、用同一名字改语义
判错标志多个消费者对"同一事件"理解不一致 / 消费者绕过 contract 直接解析

怎么用 ​

场景 1:Avro / Protobuf Schema 注册(Schema Registry) ​

yaml
# schema-registry: order-created.v1.avsc
{
  "type": "record",
  "name": "OrderCreated",
  "namespace": "com.example.orders",
  "fields": [
    {"name": "orderId", "type": "string"},
    {"name": "customerId", "type": "string"},
    {"name": "items", "type": {
      "type": "array",
      "items": {
        "type": "record",
        "name": "OrderItem",
        "fields": [
          {"name": "productId", "type": "string"},
          {"name": "quantity", "type": "int"},
          {"name": "unitPrice", "type": "double"}
        ]
      }
    }},
    {"name": "createdAt", "type": "long", "logicalType": "timestamp-millis"}
  ]
}

Schema Registry 提供的 4 个核心能力:

  1. Schema 存储——所有版本、所有命名空间
  2. 兼容性检查——新 schema 注册时自动检查兼容性
  3. Schema 演化——版本号自动管理
  4. 消费者协调——所有消费者按兼容性约束拉取

场景 2:兼容 vs 不兼容的演化规则 ​

V2 B7 段 4 演化 + B2 段 4 Hyrum's Law 视角——事件契约的兼容性规则是硬约束:

diff
✅ 兼容(可同版本演进)
+ 给 OrderCreated 加 notes 字段(optional)
+ 加新的事件类型 OrderCancelled
+ 加嵌套结构(可选)

⚠️ 危险(要协调所有消费者)
~ 给已有字段改类型(int → long)
~ 改字段名
~ 改字段默认值

❌ 不兼容(必须新主版本)
- 删字段
- 改必填为选填(如果业务上原本必填)
- 改字段语义不改名

示例——加 notes 字段的兼容演化:

diff
  {
    "orderId": "ord-001",
    "customerId": "cus-123",
    "items": [...],
-   "createdAt": 1719123456789
+   "createdAt": 1719123456789,
+   "notes": "VIP 客户优先发货"  ← 新增 optional,老消费者忽略
  }

场景 3:版本化策略(EDM Ch 3) ​

3 种主流版本策略:

yaml
策略 1: Schema Registry 自带版本(推荐)
  - v1, v2, v3 由 Registry 管理
  - 一个 topic 可同时存多个版本
  - 消费者按兼容性约束读

策略 2: Semver in topic name
  - orders.events.v1
  - orders.events.v2
  - 升级 = 新 topic + 老 topic 保留 N 月

策略 3: 内容嵌入版本号字段
  - 每个事件带 schemaVersion 字段
  - 消费者按版本路由

V2 B7 演化视角:策略 1 最优——Registry 自动管理兼容性 = 演化阻力最小 = 演化更自然。

场景 4:事件下架(Deprecation) ​

事件不能删只能停发(已发布事件是历史事实)。下架流程:

第 0 月: 公告 deprecation(spec + 内部 wiki + 邮件)
第 1 月: 监控哪些订阅者还在消费 → 通知他们迁移
第 2 月: 发布 "shutdown 预警",倒计时
第 3 月: 停止生产该事件
第 6 月: 关闭 topic(保留历史只读)

SE@G Ch 15 Deprecation 给的纪律: "Mark deprecated → Monitor usage → Provide migration path → Delete"

监控比公告更重要——公告没人看,监控指标会告诉生产方"还有人没切"。

场景 5:事件语义的"事实性"约束 ​

python
# ✅ 好的事件 = 已发生的事实
event = OrderCreated(
    orderId="ord-001",
    customerId="cus-123",
    items=[...],
    createdAt=now()  ← 事件发生时间,不可修改
)

# ❌ 坏的事件 = 命令 / 请求 / 状态查询
event = CreateOrder(...)  ← 命令不是事件
event = GetOrderStatus(...)  ← 查询不是事件
event = UpdateOrder(...)  ← 修改不是事件

事实性约束:

  • 命名 = 过去时(Created / Updated / Cancelled)
  • 时间字段 = 事件发生时的时间(不是处理时)
  • 事件内容 = 不可变快照(即使后续修改订单,事件内容也是当时状态)

IDDD Ch 8(Vernon): "A Domain Event is a full-time record of a business occurrence. It is not a notification; it is the fact itself."

常见误区 ​

  1. "Schema = Contract"——错。Schema 是结构,Contract 是"结构 + 语义 + 演化 + 下架"的完整协议
  2. "事件 = 命令"——错。事件是已发生事实,命令是请求。CreateOrder 是命令,OrderCreated 是事件
  3. "可以直接改 schema"——错。任何 schema 变更都要走兼容性检查,破坏兼容 = 主版本升级
  4. "删字段很安全"——错。事件历史里还有这个字段,老消费者依赖这个字段。删字段 = 改历史解读
  5. "deprecation 发个邮件就够了"——错。邮件没人看,要监控 + 告警 + 倒计时才能保证真下架
  6. "同一 topic 混合版本"——危险。同 topic 多版本 = 消费者要支持多版本 = 复杂度爆炸。用 Schema Registry 隔离
  7. "事件可以改状态"——错。事件是事实不是状态。改状态 = 发新事件,不是改旧事件

关键洞察 ​

事件契约不是"我答应你的格式",是"我对所有订阅者的公开承诺 + 我对历史事实的负责"。

一旦发布,事件就是历史——历史不可改,所以契约的演化必须保证历史可解读性。

V2 B7 演化的极端形态——事件流 = 业务的镜像,事件契约 = 镜像的元数据。 镜像的演化 = 元数据的演化。好的事件契约让镜像演化"对历史无损"——这是 EDM Ch 17 给出的"自然演化"的工程基础。

相关链接 ​

从名家方法论与工程化思路中蒸馏出自己的工程体系。