消息队列基础:交付、顺序、幂等、积压与死信
下单接口把“发送通知”交给后台处理,可以缩短用户等待,但也增加了一个问题:后台处理到一半崩溃怎么办?本章围绕投递、确认和重复处理讲消息可靠性,先把一条消息的生命周期弄清楚。
消息系统在生产者、broker 和消费者之间引入异步边界;可靠性来自协议、持久化、确认、幂等和运维共同作用。
1. 本文覆盖范围
- 消息模型与分区/队列
- 至少一次与幂等
- 顺序、重试和死信
- 积压、回压和模式演进
2. 核心知识详解
1. 投递链路与确认
生产确认表示 broker 已按配置接收,不代表消费者完成业务。消费者先处理后确认可减少丢失,但崩溃会重投。
- 生产端设置 id、key、schema version 和 trace。
- broker 副本、持久化和确认级别按损失预算选择。
- 消费确认只在本地事务成功后发送。
这里容易混淆的是: 所谓“发送成功”必须说明成功到哪一层、在何种副本/落盘配置下。
2. 至少一次与幂等
常见端到端语义是至少一次,因此消费者以 event id 或业务幂等键去重,并把去重记录与业务更新放在同一本地事务。
- 去重表设置唯一约束与合理保留期。
- 更新可用状态机条件或 compare-and-set。
- 外部副作用使用供应方幂等接口或内部 outbox。
这里容易混淆的是: 框架的 exactly-once 通常限定于特定 broker 读写事务,不自动涵盖任意数据库和 HTTP 服务。
3. 顺序与失败处理
顺序通常只在队列/分区内成立。选择业务 key 让同一聚合进入同一分区,并用序列号拒绝旧事件。
- 重试主题/队列采用退避,避免毒消息阻塞主队列。
- 死信包含原消息、错误、次数和可重放信息。
- 人工修复与 replay 需要审计和幂等。
这里容易混淆的是: 全局顺序显著限制并行度;业务多半只需要单实体顺序。
4. 积压与模式演进
消费者速率低于生产速率会积压。监控 lag、最老消息年龄、失败率和处理耗时,并规划扩容、暂停或丢弃策略。
- 消息 schema 采用兼容演进和消费者驱动验证。
- 大 payload 存对象存储,消息只传引用和校验信息。
- 设置 TTL 前确认过期语义和审计需求。
这里容易混淆的是: 只增加消费者数受分区数、下游容量和 key 热点限制。
3. 工程链路
sequenceDiagram
participant P as Producer
participant B as Broker
participant C as Consumer
participant D as Database
P->>B: eventId + key + schema
B-->>P: broker ack
B->>C: deliver
C->>D: 业务更新 + 去重记录(同事务)
D-->>C: commit
C-->>B: consumer ack
4. 教学示例(结合本章运行前提)
先处理业务再记录事件,必须处于同一数据库事务。existsById 只是快速判断,两个并发请求仍可能同时通过;去重表需要唯一约束,冲突时应让整个事务回滚并按消费策略处理。外部 HTTP 副作用不在这个本地事务内。
@Transactional
public void consume(OrderCreated event) {
if (processedEventRepository.existsById(event.eventId())) return;
projection.apply(event);
processedEventRepository.save(new ProcessedEvent(event.eventId()));
}
// 业务写入与去重记录处于同一数据库事务。
5. 实践与验证
- 模拟消费者提交数据库后、发送 ack 前崩溃,验证去重。
- 设计毒消息的退避、死信、修复与重放流程。
- 为订单事件定义兼容 schema 和分区 key。
6. 掌握检查
- 能描述确认边界。
- 能实现幂等消费。
- 能限定顺序范围。
- 能治理积压和 replay。