mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2311 字
6 分钟
Schema 演化与兼容性
2026-04-30

周五晚上 10 点,新版本上线——生产者开始发送带 currency 字段的 v2 格式消息。5 分钟后,还在跑 v1 代码的消费者全部报 UnknownFieldException 崩溃,订单处理全线停滞。这不是假设,这是没有 Schema 演化机制的微服务系统的必经之痛。在分布式系统中,生产者和消费者永远不可能同步升级——新代码上线时,旧代码还在运行。Schema 演化的核心问题是:如何让消息格式变更时,新旧代码都能正常工作?

一、为什么需要 Schema 演化?#

1.1 没有 Schema 的问题#

问题说明后果
无契约消息格式没有正式定义生产者和消费者对字段理解不一致
隐式耦合消费者硬编码字段名和类型格式变更导致消费失败
无法演进变更消息格式需要同步更新所有服务发布受阻
调试困难不知道消息的实际结构排查问题耗时

1.2 Schema 演化的需求#

在微服务架构中,消息格式的变更是不可避免的:

变更类型示例风险
新增字段订单增加 currency 字段低(旧消费者忽略)
删除字段移除废弃的 legacyId中(旧生产者仍发送)
修改类型amount 从 int 改为 decimal高(序列化不兼容)
重命名字段namefullName高(等价于删除+新增)
修改语义status 枚举值变更中(消费者逻辑需更新)
Note

Schema 演化的核心原则是:消费者不应该因为生产者升级 Schema 而崩溃,生产者不应该因为消费者升级 Schema 而无法发送。 这就是向前兼容和向后兼容。

二、兼容性规则#

2.1 四种兼容性#

兼容性定义说明
向后兼容(BACKWARD)新 Schema 能读旧数据新消费者能处理旧生产者的消息
向前兼容(FORWARD)旧 Schema 能读新数据旧消费者能处理新生产者的消息
完全兼容(FULL)同时向后和向前兼容最严格
无兼容(NONE)不检查兼容性最灵活但最危险
graph LR subgraph "向后兼容" OP1["旧 Producer<br/>Schema V1"] --> NC1["新 Consumer<br/>Schema V2"] Note1["V2 能读 V1 数据"] end subgraph "向前兼容" NP2["新 Producer<br/>Schema V2"] --> OC2["旧 Consumer<br/>Schema V1"] Note2["V1 能读 V2 数据"] end subgraph "完全兼容" OP3["旧 Producer"] --> NC3["新 Consumer"] NP4["新 Producer"] --> OC4["旧 Consumer"] Note3["双向兼容"] end

2.2 Avro 兼容性规则#

变更操作向后兼容向前兼容完全兼容
新增字段(有默认值)
新增字段(无默认值)
删除字段(有默认值)
删除字段(无默认值)
修改字段类型
重命名字段
// Schema V1
{
"type": "record",
"name": "Order",
"fields": [
{"name": "orderId", "type": "string"},
{"name": "amount", "type": "double"},
{"name": "status", "type": "string", "default": "CREATED"}
]
}
// Schema V2(向后兼容:新增字段有默认值)
{
"type": "record",
"name": "Order",
"fields": [
{"name": "orderId", "type": "string"},
{"name": "amount", "type": "double"},
{"name": "status", "type": "string", "default": "CREATED"},
{"name": "currency", "type": "string", "default": "CNY"},
{"name": "items", "type": {"type": "array", "items": "string"}, "default": []}
]
}

2.3 JSON Schema 兼容性规则#

JSON Schema 在 REST API 场景中广泛使用,其兼容性规则与 Avro 有所不同:

变更操作向后兼容向前兼容完全兼容说明
新增可选属性additionalProperties: true 时旧消费者忽略
新增必填属性旧生产者不会发送该属性
删除可选属性旧消费者可能引用该属性
删除必填属性同上
修改属性类型如 string → integer
缩小枚举范围旧生产者可能发送被移除的值
扩大枚举范围旧消费者不识别新增的值
修改 minimum/maximum视方向视方向放宽约束向前兼容,收紧约束向后兼容

2.4 Protobuf 兼容性规则#

变更操作向后兼容向前兼容说明
新增字段旧代码忽略新字段
删除字段旧代码使用默认值
修改字段编号破坏二进制格式
修改字段类型破坏二进制格式
修改字段名称二进制格式用编号不用名称
复用已删除编号可能读到旧数据
// Schema V1
message Order {
string order_id = 1;
double amount = 2;
string status = 3;
}
// Schema V2(兼容:新增字段,不修改已有编号)
message Order {
string order_id = 1;
double amount = 2;
string status = 3;
string currency = 4; // 新增字段,新编号
repeated string items = 5; // 新增字段,新编号
// 注意:不要复用已删除字段的编号!
// reserved 6, 7; // 标记已删除的编号
}

三、Schema Registry#

3.1 Schema Registry 架构#

Schema Registry 是消息 Schema 的集中管理服务:

graph TB P["Producer"] -->|"1. 注册 Schema"| SR["Schema Registry"] SR -->|"2. 返回 Schema ID"| P P -->|"3. 发送消息<br/>[Schema ID + 数据]"| K["Kafka Broker"] K -->|"4. 投递消息"| C["Consumer"] C -->|"5. 用 Schema ID 获取 Schema"| SR SR -->|"6. 返回 Schema"| C C -->|"7. 反序列化"| C
功能说明
Schema 存储集中存储所有 Topic 的 Schema
版本管理每个 Schema 有版本号
兼容性检查注册新 Schema 时自动检查兼容性
序列化集成与 Kafka Producer/Consumer 集成

3.2 Schema 版本生命周期#

flowchart TB P["Producer<br/>注册 Schema V2"] --> REG["Schema Registry<br/>兼容性检查"] REG -->|BACKWARD/FORWARD 兼容| STORE["存储 V2<br/>版本号递增"] REG -->|不兼容| REJECT["拒绝注册<br/>返回 409 Conflict"] STORE --> NOTIFY["通知 Consumer<br/>新版本可用"] NOTIFY --> C["Consumer<br/>按版本反序列化"] style REG fill:#e3f2fd,stroke:#1565c0 style STORE fill:#c8e6c9,stroke:#2e7d32 style REJECT fill:#ffcdd2,stroke:#c62828

3.3 Schema Registry API 实战#

# 注册 OrderEvent V1 Schema
curl -X POST http://localhost:8081/subjects/order-event/versions -H "Content-Type: application/vnd.schemaregistry.v1+json" -d '{
"schema": "{"type":"record","name":"OrderEvent","fields":[{"name":"orderId","type":"string"},{"name":"amount","type":"double"}]}"
}'
# 返回: {"id":1}
# 注册 V2 — 新增可选字段(BACKWARD 兼容)
curl -X POST http://localhost:8081/subjects/order-event/versions -H "Content-Type: application/vnd.schemaregistry.v1+json" -d '{
"schema": "{"type":"record","name":"OrderEvent","fields":[{"name":"orderId","type":"string"},{"name":"amount","type":"double"},{"name":"currency","type":["null","string"],"default":null}]}"
}'
# 返回: {"id":2}
# 查看所有版本
curl http://localhost:8081/subjects/order-event/versions
# 返回: [1, 2]
# 检查兼容性(注册前预检)
curl -X POST http://localhost:8081/compatibility/subjects/order-event/versions/latest -H "Content-Type: application/vnd.schemaregistry.v1+json" -d '{"schema": "...新 Schema..."}'
# 返回: {"is_compatible":true}
Warning

尝试注册删除必填字段的 Schema 会被拒绝——这是典型的破坏性变更。Schema Registry 的价值正在于此:在 Schema 进入生产环境之前就拦截不兼容的变更,而不是等 Consumer 反序列化失败后才发现问题。

3.2 Confluent Schema Registry#

# 启动 Schema Registry
docker run -d --name schema-registry \
-p 8081:8081 \
-e SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS=localhost:9092 \
-e SCHEMA_REGISTRY_HOST_NAME=localhost \
confluentinc/cp-schema-registry
# 注册 Schema
curl -X POST -H "Content-Type: application/vnd.schemaregistry.v1+json" \
--data '{"schema": "{\"type\":\"record\",\"name\":\"Order\",\"fields\":[{\"name\":\"orderId\",\"type\":\"string\"},{\"name\":\"amount\",\"type\":\"double\"}]}"}' \
http://localhost:8081/subjects/order-value/versions
# 查看所有 Schema
curl http://localhost:8081/subjects
# 查看 Schema 版本
curl http://localhost:8081/subjects/order-value/versions
# 查看特定版本
curl http://localhost:8081/subjects/order-value/versions/1
# 检查兼容性
curl -X POST -H "Content-Type: application/vnd.schemaregistry.v1+json" \
--data '{"schema": "...new schema..."}' \
http://localhost:8081/compatibility/subjects/order-value/versions/latest

3.3 Schema Registry 高可用#

Schema Registry 的可用性直接影响消息系统的整体可用性。Confluent Schema Registry 支持多实例部署:

# 多实例部署(通过 Kafka Topic 实现一致性)
# Schema Registry 使用 Kafka Topic "_schemas" 存储所有 Schema
# 多实例通过 Kafka 的消费者协调机制保证 Leader 选举和数据一致
# 实例 1
docker run -d --name schema-registry-1 \
-p 8081:8081 \
-e SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS=kafka:9092 \
-e SCHEMA_REGISTRY_HOST_NAME=schema-registry-1 \
-e SCHEMA_REGISTRY_LISTENERS=http://0.0.0.0:8081 \
confluentinc/cp-schema-registry
# 实例 2
docker run -d --name schema-registry-2 \
-p 8082:8081 \
-e SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS=kafka:9092 \
-e SCHEMA_REGISTRY_HOST_NAME=schema-registry-2 \
-e SCHEMA_REGISTRY_LISTENERS=http://0.0.0.0:8081 \
confluentinc/cp-schema-registry
HA 机制说明
Leader 选举多实例中只有一个 Leader 处理写请求
读请求所有实例均可处理读请求(从本地缓存)
数据存储Schema 数据存储在 Kafka _schemas Topic 中
一致性通过 Kafka 的副本机制保证 Schema 数据不丢失

3.4 Subject Name Strategy#

Schema Registry 通过 Subject 组织 Schema 版本。Subject Name Strategy 决定了 Topic 和 Schema 之间的映射关系:

策略Subject 命名规则适用场景示例
TopicNameStrategy<topic>-key / <topic>-value默认策略,一个 Topic 一个 Schemaorders-value
RecordNameStrategy<record-name>一个 Topic 多种消息类型com.example.Order
TopicRecordNameStrategy<topic>-<record-name>多 Topic 多消息类型,最精确orders-com.example.Order
// 配置 Subject Name Strategy(Producer 端)
props.put("value.subject.name.strategy",
TopicRecordNameStrategy.class.getName());
// 效果:同一 Topic 可以包含多种 Avro Record
// orders Topic 中可以同时包含 Order、Payment、Shipment 三种消息
// 每种消息有独立的 Schema 版本和兼容性策略
Note

当一个 Topic 中只包含一种消息类型时,使用默认的 TopicNameStrategy 即可。当一个 Topic 需要包含多种消息类型(事件溯源场景中很常见),必须使用 RecordNameStrategyTopicRecordNameStrategy,否则 Schema Registry 无法区分不同的 Schema。

3.5 Kafka Producer/Consumer 集成#

// Producer:使用 Schema Registry
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", StringSerializer.class.getName());
props.put("value.serializer", KafkaAvroSerializer.class.getName());
props.put("schema.registry.url", "http://localhost:8081");
KafkaProducer<String, Order> producer = new KafkaProducer<>(props);
// 发送 Avro 消息(自动注册 Schema)
Order order = Order.newBuilder()
.setOrderId("ORD-001")
.setAmount(99.9)
.setStatus("CREATED")
.build();
producer.send(new ProducerRecord<>("orders", order.getOrderId(), order));
// Consumer:使用 Schema Registry
props.put("key.deserializer", StringDeserializer.class.getName());
props.put("value.deserializer", KafkaAvroDeserializer.class.getName());
props.put("schema.registry.url", "http://localhost:8081");
props.put("specific.avro.reader", "true"); // 使用生成的 Avro 类
KafkaConsumer<String, Order> consumer = new KafkaConsumer<>(props);
consumer.subscribe(List.of("orders"));
while (true) {
ConsumerRecords<String, Order> records = consumer.poll(Duration.ofMillis(100));
for (var record : records) {
Order o = record.value(); // 自动反序列化
System.out.println(o.getOrderId() + ": " + o.getAmount());
}
}

四、序列化格式对比#

4.1 Avro vs Protobuf vs JSON Schema#

维度AvroProtobufJSON Schema
二进制格式否(文本)
Schema 演化强(默认值机制)强(字段编号)
向后兼容新增字段需默认值新增字段即可新增字段即可
向前兼容删除字段需默认值删除字段即可删除字段需默认值
代码生成支持支持(推荐)不支持
动态 Schema支持(无需代码生成)不支持支持
体积最小
人类可读
典型场景Kafka + Schema RegistrygRPC、高性能REST API

4.2 选择建议#

场景推荐格式原因
Kafka 消息AvroSchema Registry 原生支持,动态 Schema
gRPC 服务Protobuf原生支持,代码生成,高性能
REST APIJSON Schema人类可读,Web 友好
跨语言消息Avro 或 Protobuf多语言支持,二进制高效
调试阶段JSON Schema可读性好,快速迭代
Warning

不要在同一个 Topic 中混用不同的序列化格式——这会导致反序列化失败。如果必须迁移格式,创建新的 Topic 并逐步切换消费者。

五、Schema 演化最佳实践#

5.1 安全变更清单#

变更安全条件
新增可选字段有默认值
新增必填字段
删除可选字段有默认值
删除必填字段
修改字段类型
重命名字段用别名(alias)替代
修改字段编号

5.2 Schema 版本策略#

# 设置 Topic 级别的兼容性
curl -X PUT -H "Content-Type: application/vnd.schemaregistry.v1+json" \
--data '{"compatibility": "BACKWARD"}' \
http://localhost:8081/config/order-value
# 可选值:NONE, BACKWARD, BACKWARD_TRANSITIVE, FORWARD, FORWARD_TRANSITIVE, FULL, FULL_TRANSITIVE
策略说明适用场景
NONE不检查开发环境
BACKWARD新 Schema 能读旧数据消费者先升级
FORWARD旧 Schema 能读新数据生产者先升级
FULL双向兼容最安全
BACKWARD_TRANSITIVE新 Schema 能读所有旧版本长期运行
FULL_TRANSITIVE所有版本双向兼容最严格

5.3 演化流程#

graph TD CHANGE["需要变更 Schema"] --> CHECK["检查兼容性<br/>(Schema Registry 自动)"] CHECK -->|"兼容"| REGISTER["注册新版本"] CHECK -->|"不兼容"| EVALUATE["评估影响"] EVALUATE -->|"可接受"| NEW_TOPIC["创建新 Topic<br/>或新 Subject"] EVALUATE -->|"不可接受"| REDESIGN["重新设计变更<br/>使其兼容"] REGISTER --> DEPLOY["部署新版本服务"] NEW_TOPIC --> MIGRATE["数据迁移"]

六、Schema 演化实战#

6.1 场景:订单 Schema 演化#

// V1:初始订单 Schema(Avro)
@AvroSchema("{\"type\":\"record\",\"name\":\"Order\",\"fields\":[...]}")
public record OrderV1(String orderId, double amount, String status) {}
// V2:新增 currency 字段(向后兼容)
public record OrderV2(String orderId, double amount, String status, String currency) {
public OrderV2(String orderId, double amount, String status) {
this(orderId, amount, status, "CNY"); // 默认值
}
}
// V3:新增 items 字段(向后兼容)
public record OrderV3(String orderId, double amount, String status,
String currency, List<String> items) {
public OrderV3(String orderId, double amount, String status, String currency) {
this(orderId, amount, status, currency, List.of()); // 默认空列表
}
}

6.2 处理不兼容变更#

// 不兼容变更:amount 从 double 改为 BigDecimal
// 解决方案:新增字段,旧字段标记废弃
// V4:新增 preciseAmount,保留 amount
public record OrderV4(
String orderId,
@Deprecated double amount, // 保留,标记废弃
String status,
String currency,
List<String> items,
String preciseAmount // 新增:精确金额字符串
) {}
// 消费者端:优先使用 preciseAmount
public class OrderConsumer {
public void process(OrderV4 order) {
BigDecimal amount = order.preciseAmount() != null
? new BigDecimal(order.preciseAmount())
: BigDecimal.valueOf(order.amount()); // 降级到旧字段
// 处理订单...
}
}
Tip

Schema 演化的黄金法则:永远不要删除或修改已有字段,只新增字段。 如果必须”删除”一个字段,标记为 @Deprecated 并保留在 Schema 中;如果必须”修改”一个字段的类型,新增一个新字段并让消费者逐步迁移。

6.3 破坏性变更的典型场景#

以下变更在 BACKWARD 兼容模式下会被 Schema Registry 拒绝:

破坏性变更原因正确做法
删除必填字段旧 Consumer 期望该字段存在先设为可选 + default,下个版本再删除
重命名字段旧 Consumer 用旧名查找 → null新增别名字段,旧字段保留为可选
修改字段类型int → string 导致反序列化失败新增新类型字段,旧字段保留
新增必填字段(无 default)旧 Producer 不会发送该字段新增字段必须带 default 值
// 错误示范:删除必填字段
// V1: {"name": "orderId", "type": "string"}, {"name": "amount", "type": "double"}
// V2: {"name": "amount", "type": "double"} ← 删除了 orderId,BACKWARD 不兼容!
// 正确做法:先设为可选
// V2: {"name": "orderId", "type": ["null", "string"], "default": null}, {"name": "amount", "type": "double"}
// V3(等所有 Consumer 更新后): 删除 orderId

七、总结#

上一章剖析了事件溯源与 CQRS。

维度关键要点
兼容性向后兼容(新读旧)、向前兼容(旧读新)、完全兼容(双向)
Avro新增/删除字段需默认值,动态 Schema,Kafka 原生支持
Protobuf字段编号机制,新增/删除字段即可,gRPC 原生支持
Schema Registry集中管理 Schema,自动兼容性检查,与 Kafka 深度集成
最佳实践只新增字段不删除/修改,使用默认值,设置合适的兼容性策略
不兼容变更新增替代字段,旧字段标记废弃,消费者逐步迁移

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Schema 演化与兼容性
https://blog.souloss.com/posts/messaging/schema-evolution/
作者
Souloss
发布于
2026-04-30
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时