
最近折腾事件驱动架构,踩了几个坑,这篇把四种方案说清楚:Webhook、EventBridge 风格 API、Event Sourcing、CQRS。
这四种技术经常被混为一谈,但其实解决的问题完全不同。选错了要么白忙活,要么把自己坑进去。
事件驱动系统不直接请求数据或调用功能,而是通过发布「已发生的事实」来进行通信。
比如:
事件本身不告诉消费者该做什么,只传达「发生了什么」。消费者自己决定是否响应、如何响应。
现代系统往往由多个独立服务、应用和团队组成,直接通信会产生依赖。事件能降低耦合,让生产者和消费者独立演进。
常见场景:
消息系统和事件驱动系统经常一起用,但不是一回事。消息系统提供传输基础设施,事件驱动系统定义系统间如何通过事件通信和协调。
这一类包含两个相关但不同的概念。
事件驱动集成聚焦于在不同系统之间传递事件。
比如:
目标是集成和事件分发。
事件驱动架构(EDA)聚焦于应用内部如何围绕事件设计。
比如:
目标是通过事件建模、存储和处理业务行为。
一个实用判断标准:
| 技术 | 类型 | 主要目的 |
|---|---|---|
| Webhook | 事件驱动集成机制 | 当事件发生时通知外部系统 |
| EventBridge 风格 API | 事件路由与集成平台 | 跨多个系统和消费者分发事件 |
| Event Sourcing | 架构模式 | 将应用状态存储为事件序列 |
| CQRS | 架构模式 | 分离命令和查询职责 |
这些技术经常组合使用:
集成和内部架构是独立的决策——一个系统可能只需要一个,或者同时需要两个。
外部事件如何集成?
flowchart TD A1[How should events be<br/>integrated externally?] --> B1{Need simple notifications<br/>to external systems?} B1 -->|Yes| WEBHOOKS[Webhooks]
B1 -->|No — multiple consumers<br/>or systems involved| EVENTBRIDGE[EventBridge-style APIs] 内部事件如何处理?
flowchart TD A2[How should events be<br/>handled internally?] --> D{Need a complete historical<br/>record of state changes?} D -->|No| E{Need independent read and<br/>write models or optimized queries?} E -->|No| F[Traditional Architecture]
E -->|Yes| CQRSONLY[CQRS Alone] D -->|Yes| G{Need independent read and<br/>write models or optimized queries?} G -->|No| ES[Event Sourcing Alone]
G -->|Yes| ESCQRS[Event Sourcing + CQRS] 比如一个系统可能用 Webhook 做外部通知,内部却组合使用 Event Sourcing 和 CQRS——两个决策树是独立使用的,不是一个非此即彼的选择。
| 方面 | Webhook | EventBridge 风格 API | Event Sourcing | CQRS |
|---|---|---|---|---|
| 类别 | 事件驱动集成 | 事件路由与集成 | 架构模式 | 架构模式 |
| 抽象层级 | 集成机制 | 事件分发平台 | 状态管理模式 | 读写分离模式 |
| 主要目的 | 通知外部系统 | 路由和分发事件 | 将状态变更存储为事件 | 分离命令和查询 |
| 存储事件? | 否 | 有时,取决于平台 | 是 | 否 |
| 事件路由? | 基础 | 核心能力 | 否 | 否 |
| 外部集成? | 是 | 是 | 间接 | 间接 |
| 内部架构? | 否 | 否 | 是 | 是 |
| 历史重放? | 有限 | 取决于平台 | 核心能力 | 否——仅在与 Event Sourcing 配对时可用 |
| 复杂度 | 低 | 中 | 高 | 中到高 |
| 典型场景 | 第三方集成、通知 | 事件分发、云集成 | 审计、金融系统、工作流追踪 | 大规模系统、专用读模型 |
| 优势 | 简单、广泛支持、易采用 | 集中式事件路由和扇出 | 完整事件历史和可重放性 | 灵活扩展和优化的读写路径 |
| 劣势 | 路由和可靠性保证有限 | 额外的基础设施和运维复杂性 | 显著的建模和运维复杂性 | 额外的架构复杂性 |
Webhook 起源于 2000 年代中期。Webhook 从来不是正式协议,它作为一种实用的集成模式演化而来。早期的 API 靠轮询数据:
Your App |
GET /status |
No Change Wait GET /status |
No Change Wait GET /status
随着 SaaS 产品增长,轮询变得低效。企业需要更好的方式向客户通知事件。
Webhook 回答的问题是:一个系统如何在事件发生时立即通知另一个系统?
不再是:
Consumer |
"Anything new?"
反复询问,而是:
"I'll call you when
something happens."
这彻底改变了 API 集成的方式。
核心假设:消费者提供一个 URL 并等待事件。
不再是:
Client ---> Server
而是:
Client gives URL Server ---> Client
方向反转了。
假设支付成功。服务提供商发送:
POST /webhook Content-Type: application/json
Body:
{ "event": "payment.succeeded", "amount": 100
}
你的应用接收:
app.post("/webhook", (req, res) => { console.log(req.body); res.sendStatus(200);
});
输出:
{ "event": "payment.succeeded", "amount": 100
}
| 优点 | 缺点 |
|---|---|
| 极其简单 | 可靠性需要仔细处理 |
| 实时通知 | 消费者必须暴露公网端点 |
| 消除轮询 | 可能出现重复投递 |
| 使用标准 HTTP | 调试失败可能很困难 |
| 跨公司集成容易 | 没有内置重放机制 |
| 被 SaaS 平台广泛支持 | 不适合高流量场景 |
| 基础设施要求低 | 投递保证有限 |
云原生事件总线在 2010 年代后期兴起,云厂商希望将「生产者声明,消费者响应」模式formalize为托管服务。AWS EventBridge 在 2019 年推出,随后是 Azure Event Grid、Google Eventarc 等类似服务。这些平台让事件驱动集成在大规模场景下变得流行,团队有了托管的事件总线,而不必自己构建和运维。
随着系统变得更分布式,企业的架构变成了这样:
User Service | +--> Email Service | +--> Analytics | +--> Billing | +--> CRM
每个新消费者都需要再集成一次。
时间久了:
One Event |
Ten Integrations |
Twenty Integrations
变得难以管理。EventBridge 风格系统回答的问题是:服务如何只发布一次事件,就能让任何消费者响应,而不用创建直接集成?
不再是:
Producer |
Many Consumers
而是引入:
Producer |
Event Bus |
Many Consumers
生产者只知道总线。
核心假设:系统应该宣告事实,而不是调用其他系统。
不再是:
Order Service |
Call Email Service
而是:
Order Service |
OrderCreated Event |
Event Bus |
Email Service Reacts
发布事件
{ "source": "orders", "type": "OrderCreated", "data": { "orderId": 123 }
}
事件规则
{ "type": "OrderCreated"
}
目标
Email Service
当订单创建时:
Order Service |
OrderCreated |
Event Bus |
Email Service
邮件服务自动收到事件。
| 优点 | 缺点 |
|---|---|
| 服务间松耦合 | 调试和追踪更困难 |
| 容易添加新消费者 | 最终一致性 |
| 非常适合事件驱动系统 | 事件 Schema 版本管理挑战 |
| 集中式路由规则 | 可能产生隐藏依赖 |
| 云集成能力强 | 不适合请求/响应场景 |
| 组织层面可扩展 | 增加架构复杂性 |
| 支持自动化工作流 | 执行路径更难预测 |
Event Sourcing 在 2008-2012 年左右成为主流架构模式。
大多数应用只存储当前状态,历史丢失。
比如:
500 -> 700 -> 400 -> 900
更新后,只剩 900。
企业需要审计、可追溯和可重建能力,不想依赖脆弱的审计表。Event Sourcing 回答的问题是:如果我们存储每次变更而不是只存最新状态呢?
不再是:
Current Balance = 900
而是存储:
AccountOpened
MoneyDeposited(200)
MoneyWithdrawn(300)
MoneyDeposited(500)
当前状态随时可以重建。
核心假设:事件序列比当前状态更重要。
不再是:
User
------
Name: Alice
Plan: Pro
而是存储:
UserRegistered
PlanUpgraded
当前状态变成派生值。
传统 CRUD
UPDATE accounts
SET balance = 500
WHERE id = 1;
结果:
Balance = 500
历史可能丢失。
Event Sourcing
追加事件:
{ "type": "MoneyDeposited", "amount": 100
}
然后:
{ "type": "MoneyWithdrawn", "amount": 50
}
当前余额由事件计算:
+100
-50
----
50
| 优点 | 缺点 |
|---|---|
| 完整审计轨迹 | 复杂性显著 |
| 事件重放能力 | 事件 Schema 演进挑战 |
| 时间旅行调试 | 最终一致性 |
| 强合规支持 | 学习曲线陡峭 |
| 天然适合事件驱动系统 | 存储随时间增长 |
| 更容易历史分析 | 需要更多基础设施和工具 |
| 随时重建投影 | 对简单 CRUD 系统来说过度设计 |
CQRS(命令查询职责分离)由 Greg Young 在 2010 年左右提出。
大多数应用对读写使用同一个模型:
Reading Data
Writing Data
比如:
User Table | Read Users Update Users Delete Users Create Users
这在初期工作得很好。
但大型系统经常发现读流量和写流量不匹配。
比如抖音可能每秒有几百万次读取,但只有几千次写入。
CQRS 回答的问题是:如果读写被当作独立关注点处理呢?
不再是一个模型处理所有,而是引入:
Write Model |
Read Model
每一方可以独立优化。
核心假设:更新数据的方式往往和读取数据的方式不同。
比如更新订单:
Validate
Authorize
Apply Business Rules
查看订单:
Fetch
Format
Display
这些关注点不一定要放在同一个模型里。
传统 CRUD
Orders Table
用于:
SELECT *
FROM orders
和:
UPDATE orders
SET status='shipped'
同一个模型。
CQRS
写侧:
ShipOrder Command
读侧:
OrderDetails Query
两条独立路径。
Command |
Write Model
Query |
Read Model
| 优点 | 缺点 |
|---|---|
| 读写独立优化 | 复杂性显著增加 |
| 更好的可扩展性 | 最终一致性问句 |
| 领域逻辑更清晰 | 需要更多基础设施 |
| 读模型更灵活 | 调试更困难 |
| 非常适合读密集型系统 | 需要维护更多移动部件 |
| 天然适合事件驱动架构 | 学习曲线更陡 |
| 可能显著提升性能 | 对很多应用来说过度设计 |