《Java 消息中间件·事件驱动架构》1.3 事件驱动成熟度模型

把事件驱动分成五级:消息传递、领域事件、契约治理、流处理、事件溯源。逐级说明能力、代价与常见误判,给出一份 20 题、按 0/1/2 打分的自评表,并用可运行程序算出 libraryd 当前处在第 2 级、升到第 3 级还差哪两项,最后给出升级动作与不该升级的理由。

成熟度不是奖章

「我们的消息系统到第几级了」是一个容易问错的问题。问法暗示了级别越高越好,而事实是:级别越高,能力越强,成本也越高。一个只有三个人的业务团队上到事件溯源,通常不是进步,是把有限的维护能力烧在了一个不需要的复杂度上。

这一节给出一个五级模型,目的不是评级,而是诊断:看清当前能力停在哪、下一级需要补什么、补完之后要付什么账。模型按「系统能做什么」划分,不按「用了什么中间件」划分——装了 Kafka 只是第一级,和事件驱动还隔着好几步。

本节是决策框架加一个可执行的自评程序。自评的输入是对 libraryd 现状的判断,程序只负责让打分口径一致、可复算;它不会替你做判断。

五级总览

级别名称一句话能力新增的核心成本
L1消息传递能把调用异步化、削峰、解耦部署broker 运维、监控、容量
L2领域事件事件表达业务事实,多个消费者各自建读模型事件建模、幂等、最终一致
L3契约治理事件契约有事实源、兼容规则与 owner流程、工具、评审延迟
L4流处理有状态、按事件时间计算,产出持续更新的结果状态存储、再平衡、恢复演练
L5事件溯源事件日志是写模型唯一事实源,状态由 fold 得出长期 schema、重建、合规删除

五级之间不是简单的功能叠加,而是约束逐层变强。L2 要求事件是事实;L3 要求事实的格式可以被长期承诺;L4 要求这些事实可以被持续计算;L5 要求所有状态都能从事实重算。任何一层的基础不牢,上面一层都会以事故的形式还回来。

L1 消息传递

能力:生产者把消息放进 broker 就返回,消费者按自己的节奏处理。部署解耦、故障隔离、峰值缓冲都在这一级获得。

典型特征:主题按「动作」或「团队」命名,比如 send-email、order-sync;消息体可能是命令、可能是整行数据、也可能是一个 ID;消费者通常只有一个,或者多个消费者做同样的事。

这一级的合理用法:把通知、日志、非关键同步这类「丢了可以补、慢一点没关系」的工作挪出主请求路径。它已经能带来明显的可用性收益。

代价与失败模式:消息没有业务语义,消费者只能靠约定理解;生产者往往仍在等某个隐式结果;出问题时靠日志和重放,缺少契约。最常见的病是「用 MQ 做 RPC」——发一条消息,然后在另一个主题等回复。

升级信号:消费者开始按业务含义解释消息;同一件事需要多个下游各自处理;团队开始争论「这条消息到底代表什么」。

L2 领域事件

能力:事件用过去时描述业务事实,一个聚合的一次状态变化发一个事件;多个消费者各自维护读模型,互不阻塞。

典型特征:主题名是 library.loan.created 而不是 loan-sync;事件体带业务字段而不是整行表数据;消费者按业务键幂等;生产者不关心有多少消费者。

这一级需要补的三件事:

  1. 事件建模:从业务动作和状态变化里找事件,而不是从表变更里找(2.1 节)。
  2. 幂等消费:至少一次投递意味着重复必然发生,消费者要有业务键去重(5.2 节)。
  3. 接受最终一致:读模型会滞后,读路径要按需选择一致性(1.2 节的反例二)。

代价与失败模式:事件设计错了比消息设计错了更难改,因为消费者已经按错误语义建了投影;重复与乱序从「边缘情况」变成「日常情况」;排查链路跨多个消费组。

升级信号:消费者数量增长到无法靠口头同步;一次字段修改导致多个下游同时出问题;团队开始需要知道「谁在用这个事件」。

L3 契约治理

能力:事件契约有唯一事实源,变更遵守兼容规则,每个事件有 owner、SLA 和弃用流程,消费者影响面在评审时可见。

典型特征:仓库里有一份 events.yaml 或 schema registry;CI 里有消费者兼容性测试;新增字段有默认值约定;删除字段要先走弃用周期;每个主题能查到 owner。

这一级的价值:把「耦合」从运行期提前到评审期。L2 的耦合是隐性的——生产者改了字段,编译器不报错,消费者运行时才炸。L3 让这个爆炸发生在合并请求里。

代价与失败模式:流程会拖慢变更;registry 和兼容性测试需要维护;owner 制度容易退化成「挂名」。最常见的病是「有 schema 但没有兼容规则」——格式统一了,语义仍然各说各话。

升级信号:事件开始被要求做实时聚合;消费者不再满足于「存下来再查」,而是要在流上持续计算。

L4 流处理

能力:用有状态拓扑持续计算,支持窗口、join、聚合,结果写入新的主题或读模型;按事件时间处理乱序与迟到数据。

典型特征:Kafka Streams 拓扑作为一个可部署单元;有状态存储与 changelog 主题;再平衡后能从 changelog 恢复;处理语义(至少一次或恰好一次)有明确选择。

这一级不是「用了 Streams」。一个只做 filter 和 map 的无状态拓扑仍然是 L1/L2 的搬运工,没有状态、没有时间语义、没有恢复问题。L4 的门槛是状态与时间:能不能正确处理迟到数据,能不能在实例挂掉后恢复,能不能说清恢复期间的处理暂停。

代价与失败模式:状态大小决定恢复时间;再平衡会暂停处理;恰好一次只在 Kafka 内部成立,外部副作用仍要幂等;运维需要理解任务分配与 standby。

升级信号:业务要求「任何读模型都能从零重建」,或者审计要求「事件本身是唯一事实源」。

L5 事件溯源

能力:事件日志是写模型的唯一事实源,聚合状态由事件 fold 得出;任意读模型可以从零重建;有快照、upcasting 和长期 schema 演进方案。

典型特征:写路径只追加事件,不更新当前状态表;读取聚合时重放事件或读快照;新投影上线时从位移 0 重建;事件 schema 能跨多年演进。

代价:这是五级里最高的一级。事件一旦写入就很难修改,schema 演进要考虑多年;合规删除(比如个人信息)不能简单 DELETE,要用加密删除或脱敏事件;团队要理解快照、版本、重建,否则一次错误的重放就能污染读模型。

这一级不适用于大多数业务。订单、账务、审计这类「历史本身就是资产」的领域适合;内容管理、配置管理这类「只关心当前值」的领域,事件溯源带来的复杂度远大于收益。

级别是能力维度,不是组织整体

一个组织很少整体处在同一级。更常见的情况是:

能力当前级别说明
借阅链路L2领域事件 + 幂等消费
逾期报表L2批量消费,没有流处理
实时库存L1只是异步同步,仍靠定时对账
审计日志L3契约稳定,有 owner 和兼容测试
推荐特征L4有状态窗口聚合

所以「我们公司是 L2」这句话没有意义。有意义的是「这条链路在这项能力上是 L2,要升到 L3 需要补什么」。自评也必须按链路或能力分别做,否则会被最强的那个团队拉高,掩盖最弱的那个。

可执行的自评口径

每个级别四道题,每题 0/1/2 分,满分 8 分。判定规则:

0 分:没有,或完全靠人肉
1 分:部分做到,但不一致、不可验证
2 分:制度化,有工具或流程保证,可复算
某级得分 >= 6 且所有更低级别都 >= 6,才算「达到该级」
最终级别 = 连续达到的最高级

「所有更低级别都 >= 6」这条规则是为了防止跳级。一个有流处理但没有幂等消费的团队不是 L4,因为它的状态随时可能被重复事件污染;一个用了 schema registry 但没有领域事件的团队也不是 L3,因为它在治理的是「消息格式」,不是「业务事实」。

下面是打分程序的核心部分(完整类见实验工程的 exp3/MaturityScore.java,含 import 与类声明)。题目和分数是数据,规则是代码:

record Answer(String level, String id, String text, int score) {}

static final int PASS = 6; // 每级满分 8,达到 6 才算「具备」

public static void main(String[] args) {
    List<Answer> answers = List.of(
            new Answer("L1", "1.1", "生产端统一封装,业务代码不直接构造 KafkaProducer", 2),
            new Answer("L1", "1.2", "消费端有统一的提交策略与错误处理模板", 2),
            new Answer("L1", "1.3", "lag、发送失败率有监控与告警", 2),
            new Answer("L1", "1.4", "主题创建、授权、下线有流程", 1),
            new Answer("L2", "2.1", "事件用过去时命名,表达业务事实而非表变更", 2),
            new Answer("L2", "2.2", "一个聚合的一次状态变化只发一个事件", 2),
            new Answer("L2", "2.3", "消费者按业务键幂等,重复投递不产生重复副作用", 2),
            new Answer("L2", "2.4", "生产者不依赖某个消费者存在,没有隐式 RPC 回包", 1),
            new Answer("L3", "3.1", "事件 schema 有唯一事实源(目录或 registry)", 2),
            new Answer("L3", "3.2", "兼容性规则写明,且 CI 里有消费者兼容测试", 0),
            new Answer("L3", "3.3", "每个事件有 owner、SLA 与弃用流程", 1),
            new Answer("L3", "3.4", "变更评审包含消费者影响面与迁移计划", 1),
            new Answer("L4", "4.1", "有状态流处理作业(聚合、join、窗口),不只是无状态过滤", 0),
            new Answer("L4", "4.2", "状态恢复与再平衡有演练记录", 0),
            new Answer("L4", "4.3", "事件时间、迟到数据与宽限期有明确策略", 0),
            new Answer("L4", "4.4", "流作业有 SLO、容量模型与扩缩容方案", 0),
            new Answer("L5", "5.1", "事件日志是写模型的唯一事实源,状态由 fold 得出", 0),
            new Answer("L5", "5.2", "任意读模型可以从零重建并通过校验", 0),
            new Answer("L5", "5.3", "有快照、upcasting 与多年跨度的 schema 演进方案", 0),
            new Answer("L5", "5.4", "合规删除(如个人信息)有可审计的加密删除方案", 0));

    Map<String, Integer> totals = new LinkedHashMap<>();
    Map<String, Integer> max = new LinkedHashMap<>();
    for (Answer a : answers) {
        totals.merge(a.level(), a.score(), Integer::sum);
        max.merge(a.level(), 2, Integer::sum);
    }

    int reached = 0;
    for (String level : totals.keySet()) {
        int score = totals.get(level);
        boolean ok = score >= PASS;
        boolean lowerOk = reached == Integer.parseInt(level.substring(1)) - 1;
        if (ok && lowerOk) {
            reached = Integer.parseInt(level.substring(1));
        }
        System.out.printf("%s  %d/%d  %s%s%n", level, score, max.get(level),
                ok ? "PASS" : "FAIL",
                ok && !lowerOk ? " (lower level not passed, not counted)" : "");
    }

    System.out.println("final level = L" + reached);
    String next = "L" + (reached + 1);
    int gap = PASS - totals.getOrDefault(next, 0);
    System.out.println("gap to " + next + " = " + Math.max(gap, 0) + " point(s)");

    List<String> zero = new ArrayList<>();
    for (Answer a : answers) {
        if (a.level().equals(next) && a.score() == 0) {
            zero.add(a.id());
        }
    }
    System.out.println("zero-scoring items in " + next + " = " + zero);
}

跑一遍:libraryd 的自评

本机实测(Temurin 25.0.5+8 / macOS / 2026-10-29),javac --release 25 MaturityScore.java && java MaturityScore 的输出:

L1  7/8  PASS
L2  7/8  PASS
L3  4/8  FAIL
L4  0/8  FAIL
L5  0/8  FAIL
final level = L2
gap to L3 = 2 point(s)
zero-scoring items in L3 = [3.2]

结果说明 libraryd 在 L1 和 L2 上已经制度化:生产端有封装,消费端有提交模板,监控和主题流程基本齐全;事件用过去时,一个聚合一个事件,消费者按业务键幂等,生产者不依赖消费者。

L3 只有 4 分。缺的两分集中在兼容性:有 events.yaml 作为 schema 事实源(3.1 得 2 分),但没有把兼容规则写进 CI(3.2 得 0 分);owner 和评审也只是部分做到(3.3、3.4 各 1 分)。所以程序给出的缺口是 2 分,而不是「L3 还差得很远」。

这就是可执行口径的价值:它把「我们离契约治理还有多远」从一个模糊感受,变成「3.2 是 0 分,补上就是 2 分」。当然,分数本身是自评,不同人打会有一两分的差异;规则和题目固定下来,至少能保证两次评估之间可比。

让自评可信的四条规则

一份自评表很容易变成「领导在场时大家都打 2 分」。要让它有诊断价值,需要四条规则:

  1. 按证据打分,不按印象打分。 打 2 分必须能指出一个具体产物:监控面板、CI 流水线、events.yaml 里的条目、一次恢复演练记录。指不出来就是 1 分或 0 分。
  2. 多人独立打分再对齐。 生产者、消费者、运维各打一遍,差异超过 2 分的题单独讨论。分歧本身就是信息,说明「大家以为的制度」并不存在。
  3. 按链路分别打分。 不要给整个组织打一个总分;借阅链路和报表链路的能力可能差两级。
  4. 记录评估日期与版本。 三个月后重打一次,分数变化才有意义;没有基线的一次性打分只能当快照。

这四条不复杂,但大多数团队只做了第一条的一半:能指出产物,却没让不同角色独立打。结果是自评分数普遍偏高,真正的问题要等事故来暴露。

从 L2 到 L3 的三件事

动作具体做法成本完成判据
把兼容规则写进 CI用 events.yaml 生成消费者侧测试,新增字段必须有默认值,删除字段必须先弃用约一周,一次性的流水线改造改坏兼容性的合并请求会被 CI 拦下
给每个事件补 owner 与 SLA事件目录里增加 owner、consumerSla、deprecatedAt 字段半天填表,持续维护任取一个主题都能查到负责人
把影响面放进评审模板合并请求必须回答「有哪些消费组会受影响」每次评审多十分钟评审记录里能看到消费组清单

三件事里只有第一件是技术工作,后两件是流程。L3 的本质不是工具更高级,而是把隐性的消费者依赖变成显性的、可检查的记录。这也是为什么很多团队卡在 L2:写代码比推动流程容易。

四个常见误判

说法为什么不对真正的判据
「我们用了 Kafka,所以是 L2」Kafka 只提供传输,事件语义要自己建事件是否表达业务事实,是否有多个独立消费者
「我们有 schema registry,所以是 L3」格式统一不等于契约治理有没有兼容规则、owner、弃用流程、影响面评审
「我们用了 Streams 做过滤,所以是 L4」无状态过滤没有状态与时间语义是否有状态、窗口、恢复演练、事件时间策略
「我们把事件存下来了,所以是 L5」存事件只是日志,不等于事件溯源状态能否由 fold 得出,读模型能否从零重建

四个误判的共同点是把工具当成了能力。工具是必要条件,不是充分条件;每一级的判据都落在「系统能做什么、坏了能不能恢复、变更能不能控住」上。

不升级的正当理由

不升级不是落后。下列情况停在 L2 或 L1 是合理选择:

情况建议停留级别理由
业务是 CRUD,读多写少L1事件的扇出与重放用不上
团队没有平台运维能力L1–L2broker 的稳定性优先于架构先进性
强一致读是核心体验L2投影延迟不可接受,读路径要同步
领域模型还在剧烈变化L2契约治理会锁死尚未稳定的模型
合规要求「当前状态可删除」L2–L3事件溯源的删除成本很高

成熟度模型最怕被当成 KPI。一旦「今年要升到 L4」写进目标,团队就会为了指标上流处理,而不是为了问题。级别应该由业务问题拉动,不由年度规划推动。

小结

  • 五级模型是诊断工具,不是奖章;高级别意味着更强能力,也意味着更高成本。
  • L1 是异步传输,L2 是业务事实,L3 是契约治理,L4 是状态与时间,L5 是事件即事实源。
  • 每级四题、每题 0/1/2 分,6 分及格且低级别必须全部及格;跳级不算达到。
  • libraryd 自评结果:L1 7/8、L2 7/8、L3 4/8,最终 L2,距 L3 差 2 分,缺口集中在 CI 兼容性测试。
  • 工具不等于能力;「用了 Kafka / registry / Streams / 存了事件」都不构成升级理由。
  • 停在低级别可以是理性选择,前提是知道自己在放弃什么。

阅读导航:上一节 1.2 事件驱动 vs 请求驱动 | 下一节 2.1 领域事件识别

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

  1. 《Java 消息中间件·事件驱动架构》附录 B 架构决策记录模板
  2. 《Java 消息中间件·事件驱动架构》附录 A 术语表
  3. 《Java 消息中间件·事件驱动架构》15.3 全书验收