E3 - 通信契约:显式 Schema + 演化 + Registry
Schema = 通信合约;Schema 演化 = 跨版本兼容性;隐式 Schema = 隐性技术债——Shannon 信息论视角下"消息可被准确重现"是通信的根本问题。
一句话
数据契约 = 数据定义 + 触发逻辑;显式 Schema + Schema 演化 3 类型 + Schema Registry + 代码生成器——把"通信"从隐性约定升级为机器可读的合约。
这本书在这一层说了什么
本层基于 EDM Ch 3 完整精读(32KB / 5179 词)。
Bellemare Ch 3 开篇 Shannon 引言:
"The fundamental problem of communication is that of reproducing at one point either exactly or approximately a message selected at another point. —Claude Shannon"
Bellemare 关键洞察:
"In the event-driven ecosystem, the event is the message and the fundamental unit of communication. An event must describe as accurately as possible what happened and why."
核心论点 6 个:
- Event-Driven Data Contracts — 数据契约 2 组件
- Using Explicit Schemas as Contracts — 显式 vs 隐式 Schema
- Schema Definition Comments — Schema 注释的价值
- Full-Featured Schema Evolution — 演化 3 类型
- Code Generator Support — 代码生成器工作流
- Schema Registry — 注册中心作用
是什么
1. 数据契约 2 组件(Data Contracts)
Bellemare 关键定义:
"The format of the data to be communicated and the logic under which it is created form the data contract. This contract is followed by both the producer and the consumer of the event data."
2 大组件:
数据契约(Data Contract):
组件 1: 数据定义(Data Definition)
- 字段、类型、数据结构
组件 2: 触发逻辑(Triggering Logic)
- 什么业务事件触发了这个事件关键洞察:触发逻辑改变比数据定义改变更危险——它破坏事件原义。
Bellemare 关键论断:
"It is far more common to change the data definition than the triggering mechanism, as altering the latter often breaks the meaning of the original event definition."
2. 显式 Schema vs 隐式 Schema
Bellemare 关键警告:
"Any implementation of event-based communication between a producer and consumer that lacks an explicit predefined schema will inevitably end up relying on an implicit schema. Implicit data contracts are brittle and susceptible to uncontrolled change, which can cause much undue hardship to downstream consumers."
显式 Schema 的好处:
- 消费者可信赖地构建业务逻辑
- 生产者可强制类型 / 字段约束
- 机器可检查 + 自动生成代码
隐式 Schema 的代价:
- 依赖部落知识(tribal knowledge)
- 团队沟通成本随事件流数线性增长
- 不同消费者可能不一致地解释同一事件
Bellemare 关键警告(反"共享库"陷阱):
"It may be tempting to build a common library that interprets any given event for all services, but this creates problems with multiple language formats, event evolutions, and independent release cycles."
3. Schema 注释的价值
Bellemare 论断:
"Support for integrated comments and arbitrary metadata in the schema definition is essential for communicating the meaning of an event."
注释 2 大价值:
- 触发逻辑说明(文件头部)——为什么生成这个事件
- 字段上下文(字段级别)——如 datetime 是 UTC / ISO / Unix
关键:知识应该尽可能靠近事件定义,消除消费者误解。
4. Schema 演化 3 类型(Compatibility Types)
Bellemare 关键论断:
"An explicit set of schema evolution rules goes a long way in enabling both consumers and producers to update their applications in their own time."
Forward Compatibility(向前兼容)
定义:"Allows for data produced with a newer schema to be read as though it were produced with an older schema."
场景:
- 生产者先升级(用新 Schema 发数据)
- 消费者仍是旧 Schema(能读新数据,丢弃未知字段)适用:生产先变、消费者后变(最常见模式)。
Backward Compatibility(向后兼容)
定义:"Allows for data produced with an older schema to be read as though it were produced with a newer schema."
适用场景:
- 消费者期待新功能 → 用新 Schema 先更新自己
- Schema 数据存在客户端(手机 App 离线场景)→ 旧数据要用新 Schema 读
- 消费者需要重放历史数据 → 历史数据用旧 Schema 写,要用新 Schema 读
Full Compatibility(完全兼容)
定义:"The union of forward compatibility and backward compatibility."
Bellemare 关键论断:
"This is the strongest guarantee and the one you should use whenever possible. You can always loosen the compatibility requirements at a later date, but it is often far more difficult to tighten them."
关键:先 Full → 后可放宽,不可逆收紧。
Breaking Schema Change(破坏性变更)
Bellemare Ch 3 定义:
"There are times when the schema definition must change in a way that results in a breaking evolutionary change. This can happen for a number of reasons, including evolving business requirements that alter the model of the original domain, improper scoping of the original domain, and human error while defining the schema."
何时发生:
- 业务需求演化(原始域模型必须改变)
- 原始域边界划分错误(DDD 上下文理解错误)
- 定义 Schema 时的人为错误
Bellemare 关键论断:
"Breaking schema changes tend to be quite impactful for entities that exist indefinitely, but less so for events that expire after a given period of time."
——实体(永续)比事件(短暂)更受 breaking change 影响。
对实体的应对策略:
- 新 Schema 创建新实体类型
- 老实体保留旧 Schema
- 两个版本共存——消费者必须处理两种数据格式
Bellemare 关键 Tip:
"The most important thing when dealing with breaking schema changes is to communicate early and clearly with downstream consumers. Ensure that any migration plans have the understanding and approval of everyone involved and that no one is caught unprepared."
——核心是沟通 + 协调 + 共识,不是技术本身。
与 IDDD 的关系:
- DDD 强调 bounded context = 边界明确
- 跨 BC 通信应只通过 schema 契约(不是数据库)
- Breaking change 频繁 = BC 边界设计错误
Bellemare 在 Ch 3 中描述的 breaking change 场景:
- 给现有字段添加强制约束(如 nullable → required)
- 重命名字段(消费者无法识别)
- 改变字段类型(int → string)
- 删除字段(消费者依赖的字段消失)
修复路径:
- 首选:避免破坏——添加新字段/新版本,不删除老的
- 次选:双写期——新格式 + 老格式并行
- 最后:协调迁移——producer/consumer 同步升级
为什么重要:
- breaking change = 上下游团队协调成本
- 协调失败 = 静默数据丢失或服务中断
- 实体 vs 事件:实体永存,破坏性影响更长
5. 代码生成器(Code Generator)
Bellemare 工作流:
Producer 侧:
Schema 定义
↓ 代码生成器
Producer 类(含类型约束)
↓ 编译器 / 序列化器强制
事件序列化(写入 Broker)Consumer 侧:
事件从 Broker 读取
↓ 反序列化(按 Producer Schema 版本)
按 Consumer Schema 转换(应用演化规则)
↓ 代码生成器
Consumer 类(Consumer Schema 版本)
↓ 业务逻辑
处理关键洞察:代码生成器让 Schema 检查从"运行时错误"提升到"编译时错误"。
Bellemare 关键论断:
"The biggest benefit of code generator support is being able to write your application against a class definition... rather than against the raw schema."
6. Schema Registry
Bellemare 关键论断:
"The event format is stored either alongside the message, which can be prohibitively expensive at scale, or in a schema registry and accessed on-demand."
Schema Registry 作用:
- 集中存储所有 Schema
- 版本管理
- 兼容性自动检查(升级前自动验证)
- 代码生成基础
对比存消息内部:
- 消息内 = 重复存储(每条事件带 schema)→ 大小浪费
- Registry = 一次存储 + 按需获取 → 高效
为什么
为什么显式 Schema 不可省?
反例:生产者 JSON {"orderId": 123} 消费者假设 {"order_id": 123} → 字段读不到 → 业务失败。
显式 Schema:
- Producer 和 Consumer 引用同一个
.avsc/.proto文件 - 字段名驼峰 vs 下划线由 Schema 统一
- 新字段加错位置(嵌套层级)由 Schema 检查
根本:分布式系统无法靠"沟通"对齐——必须靠"机器可读的契约"对齐。
Shannon 信息论视角:
"Producer and consumer must have a common understanding of the message; otherwise, it may be misinterpreted."
显式 Schema 是"共同理解"的物理形式。
为什么 Full Compatibility 是首选?
反例:只 Forward 兼容 → Consumer 必须先升级、Producer 后升级。回滚 Producer 时 → 新数据无法被旧 Consumer 读。
Full Compatibility:
- 任意顺序升级
- 任意顺序回滚
- 旧数据 + 新数据都能被新/旧 Consumer 读
根本:分布式部署不可能"原子升级"——必须兼容任意顺序。
为什么触发逻辑改变比数据定义改变更危险?
数据定义改变:增/删字段 → 编译/运行时错误 → 立即可见。 触发逻辑改变:事件发送时机改了 → 业务语义变 → 消费者按旧语义处理 → 静默错误。
例:OrderCreated 从"提交即发"改成"支付成功发" → 库存服务按"未支付"扣库存 → 实际是"已支付" → 库存超扣。
Bellemare 关键论断:
"Altering the [triggering logic] often breaks the meaning of the original event definition."
怎么做
Step 1:定义 Schema(Avro / Protobuf / JSON Schema)
// order_created.proto
syntax = "proto3";
message OrderCreated {
string order_id = 1;
string customer_id = 2;
repeated OrderItem items = 3;
Money total_amount = 4;
google.protobuf.Timestamp created_at = 5;
}Step 2:注册到 Schema Registry
# Confluent Schema Registry HTTP API
curl -X POST http://schema-registry:8081/subjects/order_created/versions \
-H "Content-Type: application/vnd.schemaregistry.v1+json" \
-d '{"schema": "..."}'Step 3:配置兼容性级别
# 设置为 FULL(默认)或 BACKWARD
curl -X PUT http://schema-registry:8081/config/order_created \
-H "Content-Type: application/vnd.schemaregistry.v1+json" \
-d '{"compatibility": "FULL"}'Step 4:代码生成 + 类型安全
# Java + Protobuf
protoc --java_out=./src/main/java order_created.proto
# Python + Avro
avro-tools codegen python order_created.avsc > order_created.pyStep 5:CI 集成兼容性检查
# GitHub Actions 示例
- name: Schema Compatibility Check
run: |
new_schema=$(cat order_created_v2.avsc)
curl -X POST http://schema-registry:8081/subjects/order_created/versions \
-d "{\"schema\": \"$new_schema\"}"
# 注册成功 = 兼容;失败 = 不兼容Step 6:Schema 注释(业务语义)
// OrderCreated.proto
syntax = "proto3";
// Triggering Logic:
// This event is emitted when a customer successfully
// submits a shopping cart, BEFORE payment is processed.
// For payment-confirmed events, see OrderPaid.
message OrderCreated {
string order_id = 1; // UUID generated client-side
string customer_id = 2; // UUID, references Identity BC
repeated OrderItem items = 3;
Money total_amount = 4; // Final amount, includes tax
google.protobuf.Timestamp created_at = 5; // UTC, ISO 8601
}什么时候用 / 什么时候不用
用的 2 大场景
| 场景 | 关键判断 |
|---|---|
| 多团队消费同一事件流 | Producer 不能假设所有 Consumer 同步升级 → Full Compatibility 必须 |
| 事件需要持久 + 重放 | 重放历史数据时 Schema 可能已变 → 必须 Schema 演化规则 |
不用的 2 大场景
| 场景 | 原因 | 替代 |
|---|---|---|
| 单 Producer + 单 Consumer + 同代码库 | Schema 是内部类型,不需要跨进程合约 | 直接共享代码类 |
| 消息销毁(通知) | 不需要重放 → 不需要演化 | Message Broker + 简单 JSON |
看不见的地方
失效模式 1:隐式 Schema 累积技术债
症状:Producer 改字段名没通知 → 多个 Consumer 静默读错字段 → Bug 数月后才暴露。
根因:无显式 Schema → "tribal knowledge" 失效。
检测:问"Producer 改字段时,系统能否自动检测 Consumer 兼容性?"——不能,就是隐式 Schema。
修复:所有跨进程事件必须 Schema Registry + 兼容性检查。
失效模式 2:触发逻辑被悄悄修改
症状:开发改"消息发送时机"但只动了 1 行代码 → 业务语义变 → 下游服务按错假设处理。
根因:触发逻辑不在 Schema 中(无法机器检查),只在代码中。
修复:触发逻辑文档化 + 重要事件加"业务测试"(不只单元测试)。
失效模式 3:兼容性检查被绕过
症状:测试环境跳过 Schema Registry 直连 → 生产环境部署失败。
根因:CI 没集成兼容性检查。
修复:CI 必须包含 Schema 兼容性测试。
失效模式 4:删除字段而不废弃
症状:Producer v2 删除字段 → 旧 Consumer 读 null → 业务逻辑 NPE。
根因:未做"两步删除"——先废弃(保留字段但不写)+ 后删除(等所有 Consumer 升级后)。
修复:删除字段分两步:v1 标记 @Deprecated(保留默认值)+ v2 真正删除(至少 N 个版本后)。
失效模式 5:Schema Registry 单点故障
症状:Schema Registry 挂了 → Producer 无法发布新版本 → 业务停滞。
根因:未考虑 Schema Registry 高可用。
修复:Schema Registry 多副本 + 缓存本地 schema 文件(无 Registry 也能编译)。
失效模式 6:共享库反模式
症状:构建一个"事件解析共享库" → 所有服务引用 → 库升级阻塞所有服务。
根因:违反"独立发布周期"原则。
修复:每服务独立生成自己的类(从同一 Schema 生成),不共享代码。
相关节点
骨架 A — 世界观
| 节点 | 关联 | 解释 |
|---|---|---|
| wonder-02-emergence-action | 中 | Schema = "作为"前的"先行具有"——所有 Producer/Consumer 共享的视域 |
| wonder-05-concealment | 中 | 隐式 Schema = "隐匿"——隐性约定看不见但致命 |
骨架 B — 工程实践
| 节点 | 关联 | 解释 |
|---|---|---|
| change-management | 强 | Schema 演化 = 变更管理的微缩——3 兼容性类型就是变更规则 |
| quality-assurance | 中 | Schema Registry = 质量保障基础设施——自动化兼容性检查 |
| feedback-mechanisms | 弱 | CI Schema 检查 = 反馈机制——演化时立即反馈 |
与 IDDD 对位
| IDDD | EDM |
|---|---|
| Ch 8 领域事件定义 | E3 Schema 定义 |
| Ch 8 事件命名约定 | E3 触发逻辑约束(注释) |
状态
E3 概念递进落地完成(基于原书 ch03 32KB / 5179 词)。
- 6 个核心论点(数据契约 / 显式 vs 隐式 / 注释 / 演化 3 类型 / 代码生成器 / Registry)
- 6 步怎么做(含 Schema 注释完整示例)
- 6 个失效模式(含共享库反模式)
- 骨架 A/B + IDDD 完整映射