3.4 确认、重投递与顺序保障


文档摘要

3.4 确认、重投递与顺序保障 本节摘要:签收是消费者与系统之间的合同:确认了算漂完,没确认的按约重投,重投也治不好就进死信。本节拆解确认的粒度、重投的触发链、重试与死信队列的配置,再把顺序性与幂等放进同一张推理网——处理"不丢"与"不重"这对矛盾的完整工具箱都在这里。 签收这件事,比想象中讲究 消费者收到消息并处理完,要显式回一个确认(ack),Broker 才会推进该订阅的游标。没确认的消息不会消失——它在等待期后被重新投递。这个机制回答了"凭什么保证不丢":只要业务处理与确认的先后顺序写对,消息要么处理完并被确认,要么没确认而被重投,不存在第三种"悄悄丢了"的状态。 确认有粗细两种粒度。单条确认逐条回执,配合共享模式精确到条;

3.4 确认、重投递与顺序保障

本节摘要:签收是消费者与系统之间的合同:确认了算漂完,没确认的按约重投,重投也治不好就进死信。本节拆解确认的粒度、重投的触发链、重试与死信队列的配置,再把顺序性与幂等放进同一张推理网——处理"不丢"与"不重"这对矛盾的完整工具箱都在这里。

签收这件事,比想象中讲究

消费者收到消息并处理完,要显式回一个确认(ack),Broker 才会推进该订阅的游标。没确认的消息不会消失——它在等待期后被重新投递。这个机制回答了"凭什么保证不丢":只要业务处理与确认的先后顺序写对,消息要么处理完并被确认,要么没确认而被重投,不存在第三种"悄悄丢了"的状态。

确认有粗细两种粒度。单条确认逐条回执,配合共享模式精确到条;累积确认一次声明"此前全部确认",省网络开销,但共享模式下不可用(因为消息被分散到多个消费者,不存在公共的"此前")。选型口诀:独占与故障转移用累积最划算,共享与按键共享老老实实单条签。

一、消息的状态机:从投递到归宿

把一条消息在订阅视角下的生命周期画成状态机,重投与死信的位置一目了然:

状态机的每个分支都有配置入口,对应关系是:处理中到待重投的边由 ackTimeout(确认超时)触发;到重试队列的边由客户端的重试策略触发;到死信的边由 maxRedeliverCount 触发。三条边的参数相互独立,配置前先想清楚业务要哪条路径。

二、重试、死信与一次完整的坏消息处置

风控订阅的真实痛点:某些订单事件触发的第三方接口持续报错,每条都重投会无限刷屏。Pulsar 的解法是重试队列加死信队列——重试的消息先挪到旁路队列慢速消化,耗尽次数的进死信等人工。看完整配置与处置流程:

Consumer<String> consumer = client.newConsumer(Schema.STRING) .topic(TOPIC) .subscriptionName("risk-scan") .subscriptionType(SubscriptionType.Shared) .negativeAckRedeliveryDelay(5, TimeUnit.SECONDS) // nack 后等待重投间隔 .deadLetterPolicy(DeadLetterPolicy.builder() .maxRedeliverCount(5) // 最多重投五次 .retryLetterTopic("persistent://trade-order/transaction/order-events-RISK-RETRY") .deadLetterTopic("persistent://trade-order/transaction/order-events-RISK-DLQ") .build()) .subscribe(); while (true) { Message<String> m = consumer.receive(); try { handleRiskEvent(m.getValue()); // 业务处理,可能抛异常 consumer.acknowledge(m); // 成功才签收 } catch (TransientException e) { consumer.negativeAcknowledge(m); // 瞬时故障:nack,按间隔重投 } catch (PermanentException e) { // 永久故障(报文残缺等):直接 nack 让计数器推进,等它自然滑向死信 consumer.negativeAcknowledge(m); } }

处置流程跟着代码走一遍:某事件因第三方抖动失败,消费者 nack,五秒后重投;反复五次仍失败,消息被挪进 RISK-DLQ 死信主题。值班同学后来排查时,订阅死信主题把坏消息捞出来检查,修复缺陷后把修正版重新发回主主题——死信不是坟墓,是停尸房加返修口。注意区分两种失败的处理哲学:瞬时故障重投有意义,永久故障重投无意义只是刷计数器,代码里要分开表达。

三、顺序与幂等:不丢之后的两道连带题

重投保证不丢,但代价浮出水面:同一条消息可能被处理不止一次。处理超时但实际已生效、确认在网络里走丢,都会造成重复。所以"不丢"的完整工程方案必然是"至少一次投递加消费端幂等"——让重复发生时业务无感。

幂等的实现通常落在业务键上:扣库存以"订单号加动作"为幂等键,处理前查处理表,处理过就跳过。这个表不必是重装备——一条带唯一索引的记录或 Redis 的键值对都够用。配套的还有生产端的幂等:生产者重试可能造成仓里重复,Pulsar 的生产者去重功能按生产者名字与序列号在 Broker 侧丢弃重复,开一个配置就换来"发送端至少一次、实际精确一次"。

顺序性的结论在 3.1 已埋下伏笔,这里收拢成完整链路:同键同分区保证入仓顺序;按键共享保证同键同一消费者;消费端按业务键串行处理。三环扣上,"同一订单事件按产生顺序处理"才真正成立——任何一环松开(比如共享订阅),顺序承诺即刻作废,这正是许多团队从共享模式迁到按键共享的根本原因。

累积签收与批量签收:省确认开销的合法姿势

独占与故障转移模式里,累积签收值得用足。签名找最近的一个坐标,声明"到这儿为止全部确认"——Broker 只需推进一次游标,确认的网络开销从逐条变成逐批。但共享与按键共享模式没有"公共的此前"(消息散在不同消费者手里),只能单条签或用批量签收的变体:攒一批已处理成功的 MessageId 一次提交。两种省法都要守住同一条底线:没处理成功的不许混进批次——批量签收里混入失败消息,等于替它签了收,丢。

顺带回答一个高频问题:"确认丢了但业务已生效,会怎样?"会发生恰好一次的"白干":Broker 没收到签收,按超时重投,消费端幂等表发现处理过,直接再签收一次了事。这个场景正是"至少一次加幂等"组合的自愈路径——不丢由重投担保,不重由幂等吸收,两头都有人接住。理解了这条自愈链,你对"消息到底会不会丢"的焦虑可以放下一大半。

本节要点回顾

  • 确认驱动游标推进,未确认必重投,"不丢"由这个闭环担保;
  • 累积签收省开销但仅限非共享模式,共享族用单条签;
  • ackTimeout 触发超时重投,重试队列承接慢消化,死信队列收容耗尽次数的坏消息;
  • 至少一次投递必然带来重复,消费端幂等键是标配解法;
  • 顺序承诺三环扣:同键同分区、按键共享、消费端按键串行,缺一环即破功。

第 3 章收拢:消息从下水到签收的全套规则已走完。第 4 章镜头下潜,进货仓看存储的机械与账目。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U