事件驱动架构:区分事件、命令与投递保证
作者:Ai Flowi · 发布于
事件驱动架构是一种由生产者发布已发生事实、消费者根据这些事实作出响应的设计。生产者与消费者可以独立运行,通过事件投递机制连接。这种设计需要明确规定事件标识、顺序和失败处理;仅发送通知并不能确保处理可靠。
事件表示“这件事已经发生”。命令表示“请做这件事”,而请求可能期待某个特定服务直接回应。区分这些含义,有助于说明消费者是在报告观察到的情况,还是在判断某项操作是否获准。
事件驱动架构由哪些部分组成?
生产者观察到或完成一项变更后,创建事件。投递机制将事件传给关注它的消费者。每个消费者按自己的规则处理,并记录结果。消息代理可以是投递机制的一部分,但它并不定义什么是事件,本示例也不预设使用消息代理。
生产者:文档已批准
↓
事件封装 + 已批准文档的引用
↓
投递边界
├─ 消费者 A:更新业务视图
└─ 消费者 B:记录后续处理意向只有批准确实发生后,才应使用“文档已批准”这样描述事实的名称。“批准文档”是一条可以被拒绝的命令。如果把命令发布成已经完成的事件,消费者可能会依据从未发生的批准采取行动。生产者还应准确说明事件能证明什么:批准已存储、批准已请求、批准回执已发送,是不同的观察结果。
原创事件封装示例:在窄屏上,将焦点移至表格区域后横向滚动。
| 字段 | 含义 | 需要保留的边界 |
|---|---|---|
| event_id | 本次事件发生的稳定标识,例如 synthetic-approval-A。 | 同一事件再次投递时沿用该标识;新的批准不能沿用。 |
| occurred_at | 生产者记录批准事实的时间。 | 保留源时钟的上下文;该时间可能与到达时间不同。 |
| received_at | 此消费者收到本次投递的时间。 | 后续投递的接收时间更晚,但对应的仍是同一次事件发生。 |
| event_type | 在这份虚构契约中为 document.approved。 | 描述事实,而不是尚未执行的指令。 |
| schema_version | 事件封装及载荷含义的版本。 | 拒绝或复核消费者无法解释的版本。 |
| payload | 文档标识、获批修订版本及批准记录的引用。 | 仅包含获准使用的数据和响应所需的证据。 |
投递保证的范围小于业务保证
“至多一次”投递允许丢失,不允许重复投递。“至少一次”投递允许重复投递,并在所声明的条件下争取最终送达。仅凭这两个术语,都不能证明消费者恰好执行一次业务写入。应检查投递保证适用于哪一边界,以及确认消息丢失时会发生什么。
顺序也需要明确范围。一个文档的事件可以有序处理,而无关文档可以独立推进。全局接收顺序不能确定业务变更实际发生的顺序。声称具备“恰好一次”行为的系统,需要明确其适用范围和故障模型;没有证据,就不能把这一保证延伸到所有外部副作用。
推演重复投递与写入后的失败
消费者 A 收到 synthetic-approval-A,将获批修订版本写入目标后,在确认投递之前发生故障。生产者或投递机制无法得知写入已完成,因此同一事件再次到达。如果消费者无条件追加,就会创建第二条记录,尽管实际只发生过一次批准。
幂等响应是指同一事件再次处理时,仍产生预期的效果。这需要业务标识和更新规则,仅有重试计数器并不够。消费者可以将事件标识与目标端效果一同保存,或使用经过契约验证、能防止重复效果的目标端操作。先单独查询“是否见过该事件”,再进行写入,在两个副本同时到达时仍可能发生竞争。
要区分事件标识与投递尝试标识。每次重试都生成新的 event_id,会让重复检测失效。反过来,真正的新批准如果复用旧标识,可能会抑制必要的处理。这一区分由生产者负责;消费者应标记同一事件标识下相互冲突的载荷,而不能默默将其视为无害的重复。
比较两个消费者的顺序范围
这份 客户生命周期指南 给出了延迟状态的决策推演;本示例关注哪些接收顺序符合生产者声明的排序范围。
假设生产者 P 为文档 A 先发出 A1,再发出 A2,并为无关文档 B 发出 B1。这份虚构契约为每个文档分配内部顺序,不承诺不同文档之间的顺序。消费者 X 收到 A1、B1、A2。消费者 Y 收到 B1、A2、A1。两个消费者的到达记录不同,但只有 Y 的顺序违反了文档 A 内部的排序要求。
| 消费者观察结果 | 与契约的比较 | 允许的决策 |
|---|---|---|
| X:A1、B1、A2 | A1 在 A2 之前;B 不属于 A 的排序范围。 | 独立处理每个文档的事件序列。 |
| Y:B1、A2、A1 | A2 先于其必需的前序事件 A1 到达。 | 暂缓 A2 的依赖性响应;B1 可以继续处理。 |
| Y:之后收到 A1 | 缺失的前序事件现已可用。 | 按消费者的处理规则,先评估 A1,再评估暂缓的 A2。 |
| A2 声称来自生产者 Q | P 的序列无法确定 Q 的顺序。 | 合并序列前,先取得跨生产者的契约。 |
这项暂缓策略是人为制定的消费者要求,并非声称平台具备的功能。收到前序事件,不代表它已经成功处理。如果 A1 的响应失败,而 A2 的规则依赖该响应,A2 就仍然受阻。仅记录独立审计事实的消费者,可能有不同的依赖关系,因此投递顺序和处理顺序需要分别定义。
不能仅凭数字不连续就推断事件缺失,除非序列契约明确规定,中间每个编号都属于该消费者的事件流。过滤可能会有意移除某些事件类型。应记录序号覆盖一个文档、一个生产者还是整条数据流,以及消费者能否取得缺失记录。否则,在没有明确解除条件所需证据的情况下,暂缓可能变成永久阻塞。
AI Flowi Workflow 的一个具体可靠性边界
状态: 已测试 ,适用于所链接 webhook 可靠性方案记录的范围。 webhook 错误处理页面 报告称: 同一个 event_id 的五次同时投递均返回 200,并额外写入了 3 行表格记录
。HTTP 状态检查中,5 次回复中有 5 次成功,但三行记录对应同一个事件;唯一性要求未通过。这是实测的并发限制,不能证明实现了恰好一次处理。
这份 销售线索采集方案 记录了顺序重复请求更新同一行、不新增第二行的情况。其组件顺序仅能证明该方案的行为,并不代表消息代理或流处理平台。可靠性示例说明,为什么不能把顺序重复检测提升为同时投递的保证。
事件封装的数据表示接口可参阅 JSON Object Builder 和 JSON Operations;该拟议用法 已在组件目录中说明;未经 Ai Flowi 生产测试。
在以下 示例配置(并非已记录的测试)中,JSON Object Builder 的 field_names 包含表中的六个事件封装字段。如果排序契约要求,应在自定义载荷中加入 producer_id、document_id 和 document_sequence。生成的记录可以携带顺序证据,但解释这些证据仍需要消费者策略;生成 JSON 并不等于实现有序队列或消费者的原子检查点。
常见错误与相关术语
避免把指令称为事实、丢弃未知结果、把到达顺序当作业务顺序,或因为一个消费者回复就认定所有消费者都成功。应分别测试重复标识、冲突载荷、延迟修订版本及目标写入后的失败。记录每项观察结果适用于哪一边界。
工作流编排 负责协调任务中的依赖与恢复状态。 API 网关 涉及入口路由与策略。 Universal Webhook 参考文档 解释一种具体的 HTTP 入口机制,不能替代架构的投递契约。