Files
cs-note/hhs/MQ/09-流处理与事件驱动/34-事件驱动架构-EDA.md
T
2026-05-24 20:51:06 +08:00

217 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags:
- MQ
create time: 2026-05-24 19:52
---
# 事件驱动架构(EDA)
## 概述
事件驱动架构(Event-Driven Architecture, EDA)是系统间通过事件进行通信的架构风格。服务不直接调用对方,而是发布事件、订阅事件,实现松耦合和可扩展性。本文涵盖事件风暴、编排 vs 协同、Saga 模式,以及 EDA 的优势与挑战。
## 正文
### EDA 定义
在传统架构中,服务 A 需要调用服务 B,直接发起 RPC 就行。但这也意味着 A 必须知道 B 的存在、地址和接口——**紧耦合**。
EDA 换了一种思路:服务 A 只发布一个事件 "订单已创建",至于谁关心这个事件、怎么处理,A 完全不关心。服务 B、C、D 各自订阅感兴趣的事件,独立响应。
```go
// 事件:描述"发生了什么"
type OrderCreatedEvent struct {
OrderID string
UserID string
Amount float64
CreatedAt time.Time
}
// 发布事件的服务不需要知道谁会处理它
func (s *OrderService) CreateOrder(req CreateOrderReq) error {
order := s.repo.Save(req)
// 发布事件,不关心谁订阅
s.eventBus.Publish(OrderCreatedEvent{
OrderID: order.ID,
UserID: order.UserID,
Amount: order.Amount,
CreatedAt: time.Now(),
})
return nil
}
```
### 事件风暴(Event Storming)
事件风暴是一种协作式的需求分析方法论,由 Alberto Brandolini 提出。核心操作很简单:
1. 找一面大墙,用橙色便利贴写出所有**领域事件**(过去式的动词,如"订单已创建"、"库存已扣减")
2. 按时间线排列事件
3. 识别触发事件的**命令**(蓝色便利贴)和负责处理的**聚合**(黄色便利贴)
4. 发现**策略**(当某事件发生时自动触发的命令)和**读模型**
事件风暴的价值不在于产出完美的设计文档,而在于**让业务和技术人员用同一种语言对话**。一面墙上的便利贴比任何 UML 图都直观。
### 编排 vs 协同
EDA 中服务间的协作方式有两种模式:
#### 编排(Orchestration)
中心协调者指挥各服务做事,像乐队指挥。
```mermaid
graph LR
OC["编排器"] -->|"1 创建订单"| OS["订单服务"]
OC -->|"2 扣减库存"| IS["库存服务"]
OC -->|"3 发起支付"| PS["支付服务"]
OC -->|"4 安排发货"| SS["发货服务"]
style OC fill:#F44336,color:#fff
style OS fill:#2196F3,color:#fff
style IS fill:#2196F3,color:#fff
style PS fill:#2196F3,color:#fff
style SS fill:#2196F3,color:#fff
```
优点:流程清晰可见,容易调试和监控。缺点:编排器是单点,可能成为瓶颈,服务间仍然有一定耦合。
#### 协同(Choreography)
各服务自主响应事件,像舞蹈演员各自跳舞但配合默契。
```mermaid
graph LR
OS["订单服务"] -->|"OrderCreated"| IS["库存服务"]
IS -->|"StockReserved"| PS["支付服务"]
PS -->|"PaymentDone"| SS["发货服务"]
style OS fill:#4CAF50,color:#fff
style IS fill:#2196F3,color:#fff
style PS fill:#FF9800,color:#fff
style SS fill:#9C27B0,color:#fff
```
优点:真正松耦合,没有单点瓶颈,各服务可独立演进。缺点:流程分散在各服务中,难以全局理解,调试困难。
> [!question]
> 编排式 Saga 和协同式 Saga 各有什么优缺点?你在项目中会如何选择?
### Saga 模式
分布式事务的经典问题是:如何跨多个服务保证数据一致性?两阶段提交(2PC)性能差、可用性低。Saga 模式提供了一种更实用的方案:**把长事务拆成多个本地事务,每个本地事务有对应的补偿操作**。
举例:创建订单涉及三个步骤——
1. 订单服务:创建订单(补偿:取消订单)
2. 库存服务:扣减库存(补偿:恢复库存)
3. 支付服务:发起扣款(补偿:退款)
如果步骤 2 失败,执行步骤 1 的补偿操作(取消订单)。如果步骤 3 失败,执行步骤 2 和 1 的补偿操作。
**编排式 Saga**:由一个 Saga 协调者集中管理流程和补偿逻辑。协调者知道每一步该做什么、失败了怎么回滚。
**协同式 Saga**:没有中心协调者,每个服务监听事件,完成后发布新事件。失败时发布补偿事件,上游服务监听到后自行回滚。
### 事件驱动的优势
**松耦合**:生产者不依赖消费者,服务可独立开发、部署、扩展。新增一个消费者不需要修改生产者。
**可扩展**:消费者可以水平扩展,消息队列天然支持分区和并行消费。
**天然支持审计**:事件就是操作日志。任何时候都可以回放事件流,重建系统状态。这对金融、医疗等合规要求高的场景非常有价值。
### 事件驱动的挑战
**最终一致性**:事件驱动天然是异步的,不同服务看到的状态可能有短暂的不一致。需要在业务上接受"最终一致性",而不是强一致。
**调试困难**:请求链路被事件拆散,排查问题时需要在多个服务间追踪 CorrelationID。分布式链路追踪(Jaeger、Zipkin)是必备工具。
**事件风暴(过多事件)**:如果事件粒度太细、订阅关系太复杂,系统会变成"事件风暴"——满天飞的事件让人难以理解业务流程。需要在事件设计上有纪律。
### Go 代码:协同式 Saga 实现
```go
// 事件定义
type Event struct {
Type string // 事件类型
Payload interface{} // 事件数据
TraceID string // 链路追踪ID
}
// 事件总线
type EventBus struct {
subscribers map[string][]func(Event)
}
func (eb *EventBus) Subscribe(eventType string, handler func(Event)) {
eb.subscribers[eventType] = append(eb.subscribers[eventType], handler)
}
func (eb *EventBus) Publish(event Event) {
for _, handler := range eb.subscribers[event.Type] {
go handler(event) // 异步处理
}
}
// 库存服务:监听订单创建事件,扣减库存
func InventoryService(bus *EventBus) {
bus.Subscribe("OrderCreated", func(e Event) {
order := e.Payload.(OrderCreatedEvent)
err := deductStock(order.Items)
if err != nil {
// 扣减失败,发布补偿事件
bus.Publish(Event{
Type: "StockDeductFailed",
Payload: order,
TraceID: e.TraceID,
})
return
}
// 扣减成功,发布下一步事件
bus.Publish(Event{
Type: "StockReserved",
Payload: order,
TraceID: e.TraceID,
})
})
}
// 支付服务:监听库存预留事件
func PaymentService(bus *EventBus) {
bus.Subscribe("StockReserved", func(e Event) {
order := e.Payload.(OrderCreatedEvent)
err := chargePayment(order.UserID, order.Amount)
if err != nil {
// 支付失败,触发库存恢复
bus.Publish(Event{
Type: "PaymentFailed",
Payload: order,
TraceID: e.TraceID,
})
return
}
bus.Publish(Event{
Type: "PaymentDone",
Payload: order,
TraceID: e.TraceID,
})
})
}
// 订单服务:监听支付失败事件,取消订单(补偿)
func OrderCompensation(bus *EventBus) {
bus.Subscribe("PaymentFailed", func(e Event) {
order := e.Payload.(OrderCreatedEvent)
cancelOrder(order.OrderID)
fmt.Printf("Order %s compensated\n", order.OrderID)
})
}
```
这个简化实现展示了协同式 Saga 的核心:每个服务只关心自己监听的事件和需要发布的事件。失败时通过补偿事件触发回滚链。`TraceID` 贯穿整个链路,方便追踪和调试。
## 关联笔记
- [[31-MQ-背压与流控]]
- [[32-MQ-请求-回复模式]]
- [[33-MQ-与流处理]]