《Java 消息中间件·事件驱动架构》附录 B 架构决策记录模板

五份架构决策记录模板覆盖技术选型、事件契约、一致性、迁移与治理场景,每份含背景、选项对比、结论与验证条件。附纯 JDK 实现的 ADR 完整性校验器,确保决策在代码变更前落盘。

ADR 不是事后总结

ADR(Architecture Decision Record)不是项目结束后的复盘文档,而是决策之前的思考记录。如果在代码已经写完之后才补 ADR,它的价值就消失了——ADR 的核心作用是记录「为什么这么做」而不是「做了什么」。

本附录提供五份模板,覆盖技术选型、事件契约、一致性方案、迁移方案、治理方案五个场景。每份模板包含七个必填字段:标题、状态、背景、决策因素、选项对比、结论、后果、验证条件。缺失任何一个字段,ADR 就不可接受。

场景一:技术选型

标题:ADR-001 消息中间件选型

状态:已接受

背景:
系统需要引入一套消息中间件支持事件驱动架构,当前候选方案有 Kafka、RabbitMQ、Pulsar 三个。选型需要在吞吐、延迟、运维复杂度、生态成熟度之间做权衡。

决策因素:

  • 吞吐量:峰值 10K msg/s,未来三年增长到 50K msg/s
  • 延迟:事件从生产到消费的 p99 延迟 < 100ms
  • 生态:需要成熟的客户端库、监控工具、云服务托管
  • 运维:团队没有专职运维,需要低运维复杂度的方案

选项对比:

维度KafkaRabbitMQPulsar
吞吐量10K–1M msg/s1K–20K msg/s10K–500K msg/s
p99 延迟10–50ms1–10ms10–100ms
运维复杂度中(需 ZooKeeper 或 KRaft)低中(需 BookKeeper)
生态成熟度高(Confluent、云托管)中低(较新)
适合场景日志、事件流RPC、任务队列多租户、跨地域

结论:选择 Kafka。吞吐量和延迟满足当前及未来三年需求,生态最成熟,云托管选项(MSK、Confluent Cloud)降低运维压力。RabbitMQ 吞吐不足,Pulsar 生态不成熟且团队无运维经验。

后果:

  • 好处:吞吐和延迟达标,生态成熟,迁移路径清晰
  • 风险:Kafka 运维需要专门知识,前期需要培训
  • 抵消措施:选择云托管版本,减少自运维需求

验证条件:

  • 生产环境压测吞吐达到 10K msg/s,p99 延迟 < 50ms
  • 消费者 lag 在可接受范围内(< 1000 条)
  • 运维团队完成 Kafka 基础知识培训

场景二:事件契约

标题:ADR-002 事件序列化格式

状态:已接受

背景:
事件需要在生产者和消费者之间序列化传输,候选格式有 JSON、Protobuf、Avro 三种。选型需要在可读性、性能、兼容性、Schema 治理之间做权衡。

决策因素:

  • 可读性:开发调试需要人类可读的事件格式
  • 性能:序列化/反序列化延迟 < 1ms
  • 兼容性:支持字段增删不改,老消费者不崩溃
  • Schema 治理:有工具支持 Schema 版本管理和兼容性校验

选项对比:

维度JSONProtobufAvro
可读性高低(二进制)低(二进制)
序列化延迟高(文本解析)低低
包大小大(字段名重复)小(字段编号)小(Schema 共享)
Schema 演化手动管理Protobuf 支持Avro 原生支持
工具生态通用Google 生态Hadoop 生态

结论:选择 JSON + 手动 Schema 管理(v1/v2 版本号)。团队当前规模小(< 5 个服务),JSON 的可读性带来的开发效率提升大于性能损失。Schema 管理通过事件目录人工维护,不引入 Registry 降低复杂度。

后果:

  • 好处:开发调试效率高,团队学习成本低
  • 风险:包大小增加网络开销,Schema 变更依赖人工沟通
  • 抵消措施:监控消息大小,超过 10KB 时告警;Schema 变更走审批流程

验证条件:

  • 单条消息序列化/反序列化延迟 < 5ms(JSON 可接受)
  • Schema 变更后所有消费者验证通过
  • 消息大小分布 95% < 5KB

场景三:一致性方案

标题:ADR-003 跨服务事务一致性

状态:已接受

背景:
订单服务和库存服务需要协同完成「下单扣库存」操作,两个服务有独立数据库,无法使用单机事务。需要在 2PC、TCC、Saga 三种方案中选择。

决策因素:

  • 一致性强弱:允许最终一致,不强求实时一致
  • 业务场景:订单创建 + 库存扣减,扣减失败需要释放库存
  • 回滚复杂度:库存扣减的回滚操作是释放库存,简单可逆
  • 外部依赖:库存服务是内部服务,可控制 API

选项对比:

维度2PCTCCSaga(协同式)
一致性强弱强一致最终一致最终一致
业务侵入低(协议层)高(需 Try/Confirm/Cancel)中(需定义补偿事件)
回滚复杂度低(自动回滚)中(需实现 Cancel)中(需实现补偿消费者)
外部依赖需支持 XA需服务实现 TCC 接口需事件总线
性能影响高(两阶段锁)中低

结论:选择 Saga(协同式)。一致性强弱满足业务需求(最终一致),业务侵入低于 TCC,性能优于 2PC。回滚操作(释放库存)简单可逆,适合 Saga 模式。

后果:

  • 好处:性能高,业务侵入适中,回滚操作简单
  • 风险:补偿可能失败,需要人工出口;最终一致窗口期内数据不一致
  • 抵消措施:补偿操作设置超时和重试,失败时进人工处理队列;一致性窗口承诺 30 秒内

验证条件:

  • 正常流程 100% 成功
  • 库存不足场景补偿成功,无库存泄漏
  • 补偿超时场景进入人工处理,15 分钟内处理完成

场景四:迁移方案

标题:ADR-004 单体到事件驱动迁移策略

状态:已接受

背景:
现有单体系统需要拆分为事件驱动微服务,需要在「大爆炸重写」和「绞杀者模式」之间做选择。团队 8 人,业务不能停服超过 1 小时。

决策因素:

  • 停机窗口:最多 1 小时,实际要求 0 停机
  • 团队规模:8 人,无法同时维护两套系统超过 3 个月
  • 回滚能力:迁移失败需要能回滚到单体状态
  • 验证周期:每个迁移阶段需要 2–4 周观察期

选项对比:

维度大爆炸绞杀者模式
停机窗口需要(数小时–数天)0(渐进切换)
团队压力高(一次性全量)中(分阶段)
回滚能力差(数据已迁移)好(双写期间可回滚)
验证周期一次验证分阶段多次验证
技术风险高低

结论:选择绞杀者模式。满足 0 停机要求,回滚能力好,分阶段验证降低风险。虽然周期更长(预计 6 个月),但团队无法承受大爆炸的风险。

后果:

  • 好处:0 停机,回滚能力强,风险可控
  • 风险:周期延长,双写阶段需要维护两套写入逻辑
  • 抵消措施:第一阶段选最独立的模块(用户通知),双写阶段使用 outbox 模式保证一致性

验证条件:

  • 每个阶段完成后回滚演练成功
  • 双写阶段数据一致性检测零差异
  • 全部切换后单体代码库可以冻结(保留 1 个月观察)

场景五:治理方案

标题:ADR-005 事件 Schema 治理模式

状态:草稿

背景:
团队有 15 个微服务、50+ 个事件类型,Schema 变更频繁。需要在集中式 Schema Registry 和去中心化治理之间做选择。

决策因素:

  • 变更频率:每月 5–10 次 Schema 变更
  • 消费者数量:平均每事件 3–5 个消费者
  • 团队分布:3 个业务线,各自维护自己的服务
  • 兼容性要求:90% 的变更为向后兼容

选项对比:

维度集中式 Registry去中心化治理
Schema 变更审批中心化审批,流程规范各团队自主,快速灵活
兼容性校验自动(Registry 内置)手动(CI 脚本)
组织成本高(需推广和培训)低(各管各的)
一致性高(全局统一)低(可能各自为政)
适合规模中大型团队(>20 服务)小型团队(<10 服务)

结论:待定。当前规模(15 服务)处于中间地带,两种方案各有利弊。计划在 Q2 之前小规模试点集中式 Registry(选 3 个高频变更事件),Q3 评估后决策。

后果:

  • 试点好处:验证 Registry 在团队内的接受度和实际效果
  • 试点风险:增加短期复杂度,可能影响开发效率
  • 抵消措施:Registry 只管理高频事件(Top 10),其他事件保持现状

验证条件:

  • 试点 3 个事件的 Schema 变更审批时间 < 2 天
  • 消费者兼容性验证自动化率 100%
  • 团队满意度调研得分 > 4/5

ADR 完整性校验器

以下 Java 程序检查 ADR 是否包含所有必填字段,以及状态转换是否合法。

import java.util.*;

public class ADRValidator {

    record ADR(String id, String title, String status, String background,
                 String factors, String options, String conclusion,
                 String consequences, String validation) {}

    static final Set<String> VALID_STATUSES = Set.of("草稿", "已接受", "已废弃");
    static final Map<String, List<String>> VALID_TRANSITIONS = Map.of(
        "草稿", List.of("已接受", "已废弃"),
        "已接受", List.of("已废弃"),
        "已废弃", List.of()
    );

    static List<String> validate(ADR adr) {
        List<String> errors = new ArrayList<>();
        if (isBlank(adr.title())) errors.add("标题缺失");
        if (isBlank(adr.status())) errors.add("状态缺失");
        if (!VALID_STATUSES.contains(adr.status())) errors.add("状态非法: " + adr.status());
        if (isBlank(adr.background())) errors.add("背景缺失");
        if (isBlank(adr.factors())) errors.add("决策因素缺失");
        if (isBlank(adr.options())) errors.add("选项对比缺失");
        if (isBlank(adr.conclusion())) errors.add("结论缺失");
        if (isBlank(adr.consequences())) errors.add("后果缺失");
        if (isBlank(adr.validation())) errors.add("验证条件缺失");
        return errors;
    }

    static boolean isBlank(String s) { return s == null || s.trim().isEmpty(); }

    static boolean canTransition(String from, String to) {
        List<String> allowed = VALID_TRANSITIONS.getOrDefault(from, List.of());
        return allowed.contains(to);
    }

    public static void main(String[] args) {
        ADR good = new ADR("ADR-001", "消息中间件选型", "已接受",
            "需要引入事件驱动架构", "吞吐、延迟、运维",
            "Kafka vs RabbitMQ vs Pulsar", "选 Kafka",
            "好处:吞吐达标;风险:运维复杂", "压测通过");

        ADR bad = new ADR("ADR-002", "", "草稿",
            "需要选序列化格式", "", "JSON vs Protobuf", "选 JSON",
            "好处:可读性高", "");

        ADR invalidTransition = new ADR("ADR-003", "一致性方案", "已废弃",
            "跨服务事务", "一致性强弱", "2PC vs Saga", "选 Saga",
            "性能好", "验证通过");

        System.out.println("=== ADR 完整性校验 ===\n");

        System.out.println("[ADR-001] 完整案例:");
        List<String> e1 = validate(good);
        System.out.println(e1.isEmpty() ? "✓ 通过" : "✗ " + e1);

        System.out.println("\n[ADR-002] 缺失字段案例:");
        List<String> e2 = validate(bad);
        e2.forEach(err -> System.out.println("  ✗ " + err));

        System.out.println("\n[ADR-003] 状态转换检查:");
        boolean ok = canTransition("已接受", "已废弃");
        System.out.println("已接受 → 已废弃: " + (ok ? "✓ 合法" : "✗ 非法"));

        boolean badTrans = canTransition("已废弃", "已接受");
        System.out.println("已废弃 → 已接受: " + (badTrans ? "✓ 合法" : "✗ 非法"));

        System.out.println("\n校验规则:");
        System.out.println("- 七个必填字段: 标题、状态、背景、因素、对比、结论、后果、验证");
        System.out.println("- 合法状态: 草稿、已接受、已废弃");
        System.out.println("- 合法转换: 草稿→已接受/已废弃, 已接受→已废弃");
    }
}

本机实测:javac --release 21 ADRValidator.java && java ADRValidator

预期输出:

=== ADR 完整性校验 ===

[ADR-001] 完整案例:
✓ 通过

[ADR-002] 缺失字段案例:
  ✗ 标题缺失
  ✗ 决策因素缺失
  ✗ 验证条件缺失

[ADR-003] 状态转换检查:
已接受 → 已废弃: ✓ 合法
已废弃 → 已接受: ✗ 非法

校验规则:
- 七个必填字段: 标题、状态、背景、因素、对比、结论、后果、验证
- 合法状态: 草稿、已接受、已废弃
- 合法转换: 草稿→已接受/已废弃, 已接受→已废弃

ADR 的使用流程

五份模板的实际使用流程:

步骤做什么责任人产出物
1. 识别决策点在架构设计中发现需要记录的技术选择架构师决策候选列表
2. 填写 ADR用模板填写七个字段,状态设为「草稿」架构师ADR 文档
3. 评审技术评审会讨论 ADR,确认选项对比是否完整评审组评审意见
4. 状态更新根据评审意见更新为「已接受」或「已废弃」架构师更新后的 ADR
5. 验证按验证条件执行检查,结果写入 ADR开发团队验证报告
6. 归档已接受的 ADR 归档到版本仓库架构师归档文件

关键原则:ADR 在代码变更之前写,变更之后只补验证结果。如果代码已经写完了才补 ADR,就是事后总结,不是事前决策。

模板选型的指导

五个场景对应书稿的不同章节:

ADR 场景书稿章节核心决策点
技术选型卷一第 1 章Kafka vs 其他消息队列
事件契约卷三第 2.3、6 章Schema 格式、兼容性策略
一致性方案卷三第 4 章2PC vs TCC vs Saga
迁移方案卷三第 12.2 章大爆炸 vs 绞杀者
治理方案卷三第 11 章集中式 vs 去中心化

ADR 不是每章都需要的,只在出现技术分歧时才写。写 ADR 的目的是记录决策上下文,让半年后回顾时知道「为什么当时这么选」。

本节小结

  • ADR 是事前决策记录,不是事后总结,必须在代码变更之前写。
  • 五份模板覆盖技术选型、事件契约、一致性、迁移、治理五个场景。
  • 每个 ADR 必须包含七个字段:标题、状态、背景、因素、对比、结论、后果、验证。
  • 状态转换规则:草稿→已接受/已废弃,已接受→已废弃,已废弃不可恢复。
  • 本机实测:ADRValidator 校验完整性,缺失字段和非法状态转换均被拦截。
  • ADR 的使用时机:出现技术分歧、选型犹豫、团队意见不统一时。

阅读导航:上一节 附录 A 术语表

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《Java 消息中间件·事件驱动架构》附录 A 术语表
  2. 《Java 消息中间件·事件驱动架构》15.3 全书验收
  3. 《Java 消息中间件·事件驱动架构》15.2 故障演练剧本