架构(05):消息驱动架构——事件驱动、编排 vs Choreography、死信、幂等消费
更新时间:2026-09-02。本文是
architecture/架构领域第 05 篇,接 CQRS 与 Event Sourcing。消息驱动架构通过异步消息解耦生产者和消费者,削峰填谷,提升系统伸缩性。核心问题是:怎么保证消息不丢、怎么处理重复、怎么保证顺序、怎么处理死信。
本文要回答的问题
- 消息驱动和同步调用有什么区别?什么时候用消息驱动?
- 编排(Orchestration)和 Choreography(路由编排)区别是什么?哪个好?
- 消息怎么保证不丢?至少一次 vs 恰好一次?
- 消费重复怎么办?怎么实现幂等?
- 死信队列用来做什么?什么时候会进死信?
一、消息驱动 vs 同步调用
同步调用(REST/gRPC)
生产者 → 同步调用 → 消费者
生产者阻塞等待返回
优点:
简单,实时响应
错误直接返回
缺点:
耦合,生产者依赖消费者
消费者慢了,生产者也慢
不能削峰,流量打满就雪崩消息驱动(异步消息)
生产者 → 发消息到 Broker → 消费者拉取
优点:
解耦,生产者不依赖消费者
削峰填谷, Broker 缓冲流量
伸缩性好,消费者可以水平扩展
异步处理,不需要实时返回
缺点:
不实时,延迟增加
复杂度高(消息丢失、重复、顺序)
调试难
需要额外维护 Broker(Kafka/RocketMQ/RabbitMQ)适用场景:
| 场景 | 推荐 |
|---|---|
| 非实时操作,不需要立即返回 | 消息驱动 |
| 流量波动大,需要削峰 | 消息驱动 |
| 服务间解耦,不需要同步依赖 | 消息驱动 |
| 需要实时响应,用户等待 | 同步 |
| 强一致性,需要立即返回结果 | 同步 |
二、Saga 编排:Orchestration vs Choreography
Saga 处理分布式长事务,两种编排方式:
1. Orchestration(中心化编排)
一个协调器(Orchestrator)控制整个流程
┌──────────────┐
│ Orchestrator │ ← 事件
└───┬──────────┘
│ 发命令
↓
┌──────────┐ ┌──────────┐ ┌──────────┐
│ ServiceA │ │ ServiceB │ │ ServiceC │
└──────────┘ └──────────┘ └──────────┘
特点:
- 协调器知道整个流程
- 服务只做自己的事,不知道整个流程
- 适合复杂流程,容易修改
- 中心化,单点依赖协调器2. Choreography(事件编排,无中心)
每个服务监听事件,自己决定下一步
ServiceA 完成 → 发 EventA → ServiceB 监听 → 完成 → 发 EventB → ServiceC 监听
┌─────────┐ ┌─────────┐ ┌─────────┐
│ ServiceA│ → │ ServiceB│ → │ ServiceC│
└─────────┘ └─────────┘ └─────────┘
特点:
- 去中心化,没有协调器
- 每个服务自己知道什么时候做
- 松耦合,每个服务独立
- 流程分散,不好理解,不好调试对比:
| 对比 | Orchestration | Choreography |
|---|---|---|
| 中心化 | 是 | 否 |
| 复杂度 | 协调器复杂,服务简单 | 协调简单,每个服务逻辑复杂 |
| 可理解性 | 流程集中,好理解 | 流程分散,难理解 |
| 松耦合 | 服务之间不直接依赖 | 非常松耦合 |
| 适合 | 流程复杂,容易修改 | 流程简单,松耦合 |
选型建议:
- 流程复杂 → Orchestration
- 简单流程,松耦合 → Choreography
- 微服务、Saga → 推荐 Choreography,更符合微服务
三、消息可靠性:怎么保证不丢
丢消息的三个环节:
1. 生产者 → Broker:生产者发送失败
2. Broker 存储:Broker 宕机丢消息
3. Broker → 消费者:消费者处理失败,没 ack
解决:
1. 生产者:发送确认,失败重试
2. Broker:持久化存储,副本
3. 消费者:手动 ack,处理完才 ack
结果:至少投递一次(At Least Once)
恰好一次(Exactly Once)很难,需要幂等至少一次 vs 恰好一次:
- 至少一次:可能重复,需要消费者幂等
- 恰好一次:性能差,复杂度高
- 实践:大部分场景用至少一次 + 幂等,比恰好一次简单可靠
四、消费重复:幂等消费
为什么重复:
Broker 没收到 ack → 重发 → 消费者收到重复
解决:幂等,同一个消息处理多次结果一样
幂等实现方式:| 方式 | 说明 | 适用场景 |
|---|---|---|
| 唯一消息 ID + 去重表 | 处理完把 ID 存去重表,重复直接跳过 | 任何场景,最通用 |
| 幂等设计 + 数据库乐观锁 | 更新前检查版本号,版本不对不更新 | 更新操作 |
| 天然幂等 | 操作本身就是幂等(比如设置状态成功) | 状态机 |
| 唯一约束 | 数据库唯一键,重复插入跳过 | 插入操作 |
通用做法: 每个消息分配唯一 ID,处理前查去重表,有就跳过,没有就处理然后插入 ID。
五、顺序消费
问题:消息要按顺序处理(比如订单状态变化)
乱序 → 状态错了
解决:
1. 发:同一个订单的消息发去同一个 Partition
2. 消费:同一个 Partition 只派一个消费者消费
3. 处理完一个再处理下一个,保证顺序
问题:吞吐量下降,因为单 Partition 单线程
解决:按业务维度拆分 Partition,多个维度平行处理
示例:
订单 ID % N → Partition ID
同一个订单总是同一个 Partition
同一个 Partition 单线程消费
保证同一个订单消息顺序六、死信队列
死信(Dead Letter):消费失败多次的消息,放到死信队列
为什么会进死信:
1. 消息格式错误
2. 依赖服务不可用,一直失败
3. 业务逻辑异常
为什么需要死信队列:
不让坏消息一直重试,占住消费者
坏消息隔离,不影响好消息
事后人工处理死信
流程:
消费者处理失败 → 重试 N 次还是失败 → 移到死信队列 → 人工排查 → 修复后重发实践:
- 每个业务队列配一个死信队列
- 重试次数:一般 3 次,最多 5 次
- 死信队列需要监控,报警
七、重试策略
失败了怎么重试?
1. 立即重试:前几次立即重试
适合:瞬时错误(网络抖动)
2. 退避重试:指数退避,1s → 2s → 4s → 8s
适合:依赖服务负载高,给时间恢复
3. 最大重试次数到了 → 死信队列
不一直重试,不影响其他消息八、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| 不做幂等 | 重复消费,数据错了 | 任何消费都做幂等 |
| 消费完不 ack | 一直阻塞,消息不推进 | 处理完一定要 ack,失败 nack |
| 乱序不处理 | 业务状态错了 | 按业务维度分区,单线程消费 |
| 没有死信队列 | 坏消息一直重试,占满消费能力 | 配置死信队列,监控报警 |
| 重试不指数退避 | 一直重试把依赖打垮 | 指数退避重试 |
| 过度异步 | 所有操作都异步,延迟高,调试难 | 简单操作用同步,复杂长事务异步 |
相关与延伸
下一篇:分布式一致性——CAP、BASE、Raft/Paxos、分布式事务方案对比;CQRS,见 CQRS 与 Event Sourcing——读写分离、事件溯源。
一句话总结
消息驱动架构:异步消息解耦生产者和消费者,能削峰填谷提升伸缩性,但增加复杂度;Saga 长事务编排:Orchestration 中心化协调适合复杂流程,Choreography 事件驱动去中心化适合微服务松耦合;可靠性:至少一次投递 + 消费者幂等,比恰好一次简单可靠;幂等:唯一消息 ID + 去重表最通用;顺序消费:同一个业务发同一个分区,单线程消费;死信队列:消费失败 N 次后移过去,隔离坏消息,人工处理;重试:指数退避,到次数进死信;适用场景:非实时、流量波动大、需要解耦,不适用实时强一致。