先把失败分好类
消费失败不是一个东西,硬套同一种处理方式是灾难的开始。先分类:
- 瞬时失败:数据库锁冲突、下游接口抖动、网络超时——等一会儿再试,大概率能成,重试有意义;
- 永久失败:消息体缺字段、业务代码有 bug、订单压根不存在——试一万次也是白搭,重试无意义。
理想世界应该按失败类型路由,现实里 MQ 分不出来。于是工业界的共识做法是:有限次自动重试,重试耗尽转死信,死信靠人工。
RocketMQ 的重试机制拆解
消费返回 RECONSUME_LATER 或抛异常,RocketMQ 不会原地重试,而是把消息投进这个消费者组专属的重试队列(%RETRY%组名),按延迟等级排队后重新投递:
- 重投延迟逐次拉长:10s、30s、1m、2m、3m……一路递增到 2h——这就是退避(backoff),给故障的下游留恢复时间,避免一秒钟轰炸十六次;
- 默认最多重试 16 次,全程约 2 小时;
- 16 次全失败,消息进入死信队列(%DLQ%组名),退出自动重投的循环。
注意重试的另一面:第 7 篇说过,重试就是重复投递,幂等是重试机制成立的前提,没有幂等的重试是给事故加滤镜。
Kafka 的空白区
Kafka 没有内置的重试队列和死信,失败消息拉不拉、重不重、丢不丢,全看消费代码怎么写。社区的标准做法是自建重试 Topic:失败后按延迟档位发到 retry-5s、retry-30s、retry-1m 等主题,消费耗尽后进 dead-letter-topic。Spring Kafka 的 @RetryableTopic 把这套「非阻塞重试」封装成了注解,值得直接用。用 Kafka 做业务消息的团队,这块基建要自己补——这也是第 3 篇说「业务消息优先 RocketMQ」的另一个理由。
死信队列不是垃圾场,是待办箱
最怕的死信姿势是「从不看它」。死信里躺着的每一封都是「系统承认处理不了」的欠条,必须有人签收:
- 监控告警:死信堆积量大于 0 就告警,这是死信管理的第一行代码;
- 排查归类:看消息体、看异常栈,分清是 bug、脏数据还是外部依赖故障;
- 修复后重放:bug 修了、数据补齐了,把死信消息重新投回原 Topic 重放。重放前确认消费逻辑已带幂等,重放等于一次批量重复投递;
- 兜底登记:确实无法恢复的(业务已回滚等),人工登记后放行,让账面对得平。
毒消息自救 SOP
顺序消费章节提过毒消息卡队列的雷,这里给一套完整 SOP:
| 阶段 | 动作 |
|---|---|
| 发现 | 同一 msgKey 反复失败、死信告警触发、队列消费位点长时间不动 |
| 隔离 | 临时跳过(记录 msgKey 与位点),先恢复队列水位,避免连坐 |
| 修复 | 本地复现:补数据、修 bug、修依赖 |
| 重放 | 控制台或脚本定向重发,监控消费结果确认闭环 |
重试设计清单
- 重试次数和退避间隔按下游恢复速度定,业务默认(16 次,2 小时)通常够用;
- 重试消息的异常和上下文要打全日志,msgKey 贯穿始终;
- 死信告警是硬性要求,处理时效写进值班手册;
- 重放工具提前备好,别等出事现场写;
- 重试与幂等成对出现,评审时一起过。
小结
消费失败的治理公式:瞬时失败靠退避重试,永久失败靠死信人工,全程靠幂等保平安。重试队列是缓冲带,死信队列是待办箱——只要告警响得够及时,两者都不可怕。
单条消息的毛病说完了,下一篇说成片的问题:消息积压。十万条消息堆在队列里,消费者扁了,这时候先做什么、后做什么、哪些动作是饮鸩止渴?
评论 (0)