首页/开发/系统分析与架构设计速查/收件箱模式(Idempotent Consumer)

收件箱模式(Idempotent Consumer)

消费端记录已处理的消息标识(inbox 表),与业务变更同事务提交;重复投递时跳过处理,保证“至少一次投递”下的消费效果不重复。

收件箱模式解决什么

消息系统通常只保证“至少一次”投递,重复消息是常态而不是异常。收件箱模式(幂等消费者)的做法是:消费端维护一张已处理消息表,把消息标识的写入与业务变更放在同一个本地事务里,重复投递时因唯一约束冲突而跳过业务处理。这样即使上游重复发送或消费者重启后重读位点,业务效果也不会被重复执行。

基本信息

记录时机与业务写入同一事务,保证原子
判断消息 ID 已存在则跳过,但需更新消费位点
配套失败重试与死信队列
表结构消息 ID 唯一约束、消费者标识、处理时间,用于去重与排查

典型使用场景

  • 支付回调处理:同一笔支付可能收到多次通知,只能入账一次
  • 订单状态流转:重复的“支付成功”消息不能把订单推进两次
  • 积分与券发放:重复消息会导致多发,必须按消息 ID 去重
  • 对账与结算:上游补发历史消息时,已处理部分应被识别并跳过
  • 跨系统同步:消费端从多来源收消息,用统一去重表收敛

实施步骤

  • 建表:inbox(message_id 去重约束, consumer, processed_at)
  • 同事务:业务变更与 inbox 插入在同一事务提交
  • 判定:去重约束冲突即已处理,跳过业务并提交消费位点
  • 清理:已处理记录按时间归档,控制表体积与查询开销
  • 异常:业务失败不写 inbox,交由重试与死信流程处理
  • 位点提交:所有处理分支都要提交位点,避免重复投递循环

边界情况

跨库场景先写本地 inbox,再以本地事务驱动业务写入,或改用本地消息表方案
并发注意同一消息并发到达时依赖数据库约束兜底,而非先查后写的应用逻辑
消息 ID 来源优先使用生产者生成的全局唯一 ID;仅在消费端生成则无法跨实例去重

处理流程(伪代码)

处理流程(伪代码)
begin tx
  insert into inbox(message_id, consumer) values (?, ?)
    -- 唯一约束冲突 → 已处理,直接提交位点并返回
  apply_business_change(msg)      # 与 inbox 同事务
  update_offset(msg.offset)
commit
# 业务异常:整体回滚,不写 inbox,交由重试或死信处理
inbox 与 outbox 常成对出现,构成可靠消息链路的两端:生产端用 outbox 保证消息一定发出,消费端用 inbox 保证只生效一次。
去重判定的可靠性来自数据库唯一约束而不是“先查询再插入”:并发场景下先查后写会同时通过检查,必须让唯一约束在提交时兜底。

易错点

  • 先处理业务后写 inbox,中间崩溃仍会重复处理,去重形同虚设
  • 跳过重复消息时忘记提交消费位点,消息被反复投递形成死循环
  • 去重依赖应用层的先查后写,并发场景下两条相同消息同时通过检查
  • inbox 表只增不清理,体积膨胀后插入与查询都变慢
  • 业务失败也写入 inbox,重试时被判为已处理,消息被静默丢弃

数据来源与参考

  • microservices.io 模式目录
  • 《Enterprise Integration Patterns》:Idempotent Receiver
​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌