RabbitMQ 可靠消息:确认、重试、死信与幂等消费

可靠消息链路至少包含四段:业务事务产生消息、生产者发送到 Broker、Broker 路由并持久化、消费者处理并确认。只把队列声明为 Durable,不能覆盖其中任何一段失败。

生产端:事务与消息不能双写

数据库提交成功但发送失败,会丢消息;先发送再提交,则消费者可能读到不存在的业务状态。常见解法是 Transactional Outbox:业务数据和事件记录在同一数据库事务提交,后台 Publisher 再投递事件。

Publisher 必须开启 Confirm,并为每条消息关联业务事件 ID。Confirm 说明 Broker 已接收并按配置处理,不代表消费者已经完成业务。对于无法路由的消息,交换机配置和 Mandatory Return 也要处理,不能让消息静默丢弃。

队列与消息持久化

队列声明 Durable,消息设置 Persistent,并使用具备合适持久化语义的队列类型。持久化仍受磁盘、集群和确认策略影响,因此要通过故障演练验证,而不是只看配置字段。

消费端:成功后再 ACK

关闭自动确认。只有数据库事务等业务操作成功后才 ACK:

receive -> validate -> execute transaction -> commit -> ack

处理失败时不要无限 nack(requeue=true),它会制造热循环。区分错误:

  • 临时错误:进入有延迟的重试队列,指数退避并限制次数。
  • 永久业务错误:记录原因后进入死信或直接拒绝。
  • 代码异常:告警并保留消息上下文,不能无限快速重投。

Prefetch 控制单个消费者尚未确认的消息数。值过大可能让一个消费者囤积消息并放大故障损失;值太小则吞吐不足。应按单条处理时间、资源占用和并发能力压测。

消费必须幂等

网络断开可能发生在业务已提交、ACK 尚未到达 Broker 之间,因此重复投递是正常情况。使用事件 ID 建立消费记录:

INSERT INTO consumed_event(event_id, consumer, create_time)
VALUES (?, ?, NOW());

让唯一键与业务更新处于同一事务。插入冲突表示已经处理,可以直接 ACK。仅在 Redis 设置一个短 TTL Key,可能在过期或缓存丢失后再次执行,不适合重要业务。

死信不是垃圾桶

死信消息必须带原始事件 ID、业务类型、失败次数、最后错误和时间。后台提供查询、修复和人工重放;重放也要经过幂等校验。定期统计死信增长速度,而不是等队列撑满才处理。

可靠性验收场景

  1. 发送前后分别终止生产者,消息是否最终可见?
  2. Broker 重启后,已确认消息是否仍在?
  3. 消费事务提交后立刻断网,重复消息是否不会重复扣款或创建数据?
  4. 下游持续失败时,是否按间隔重试并最终进入死信?
  5. 单条毒消息是否会阻塞整个队列?
  6. 重放死信是否可审计、可暂停?

参考资料