news 2026/10/15 13:33:04

死信队列什么时候该用:3 类必须进 DLQ 的消息与 2 种重试策略

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
死信队列什么时候该用:3 类必须进 DLQ 的消息与 2 种重试策略

我不会起名字322· 后端 / 算法 / 数据库

📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL

文章目录

  • 死信队列什么时候该用:3 类必须进 DLQ 的消息与 2 种重试策略
    • 一、先说一个反面案例
    • 二、3 类必须进 DLQ 的消息
      • 第 1 类:格式错误 / 无法解析的消息(poison message)
      • 第 2 类:业务上不可能成功的消息
      • 第 3 类:重试次数耗尽的"真失败"
    • 三、不该进 DLQ 的:瞬态失败
    • 四、2 种重试策略
      • 策略 1:延迟队列(分级重试)
      • 策略 2:应用层重试 + 自建重试表
      • 两种策略怎么选
    • 五、DLQ 必须做的三件事
      • 1. 监控 DLQ 深度
      • 2. DLQ 消息要带上下文
      • 3. 要有重放工具
    • 六、带走物

死信队列什么时候该用:3 类必须进 DLQ 的消息与 2 种重试策略

一、先说一个反面案例

很多队伍的做法是:所有消费失败的消息都进重试队列,重试 N 次后进死信队列,然后……就没有然后了。

死信队列里堆了几万条消息没人看,等于把问题藏起来了。这比不配死信队列更糟——你以为有兜底,其实没有。

死信队列的价值不在于"接收失败消息",而在于:把"不可自动恢复"的消息从主链路里隔离出去,让主链路不被拖垮,同时让这部分消息可被人工/专项处理。

所以关键问题是:什么样的消息该进 DLQ?

二、3 类必须进 DLQ 的消息

第 1 类:格式错误 / 无法解析的消息(poison message)

这类消息重试一百万次也不会成功:

  • JSON 反序列化失败;
  • 必填字段缺失;
  • 版本号不认识(生产者升级了但你还没兼容)。
funchandle(msg[]byte)error{varevt Eventiferr:=json.Unmarshal(msg,&evt);err!=nil{// 解析失败 → 重试毫无意义,直接进 DLQreturnErrPoison// 标记为不可重试}...}

这类消息最危险的地方在于它会阻塞主链路:如果它一直重试,消费位点就一直卡在那里(对 RocketMQ 尤其明显),后面几万条正常消息全部被堵住。

第 2 类:业务上不可能成功的消息

比如下单消息里的商品 ID 在数据库里根本不存在、用户已被注销、金额是负数。

这类和格式错误的区别是:消息本身是合法的,但业务状态决定了它不可能成功。重试同样没有意义。

判断依据:失败原因是"确定性"的还是"瞬态"的。

失败原因性质处理
JSON 解析失败确定性直接 DLQ
商品不存在确定性直接 DLQ + 告警
数据库连接超时瞬态重试
下游返回 503瞬态重试
限流瞬态重试(带退避)
参数校验失败确定性直接 DLQ

代码里必须能区分这两类。最实用的做法是用错误类型而不是错误字符串:

// 定义可重试与不可重试两类错误typePermanentErrorstruct{Errerror}// 不可重试 → DLQfunc(e*PermanentError)Error()string{returne.Err.Error()}typeTransientErrorstruct{Errerror}// 可重试 → 重试队列func(e*TransientError)Error()string{returne.Err.Error()}funchandle(msg Msg)error{evt,err:=parse(msg.Body)iferr!=nil{return&PermanentError{err}// 解析失败,永久错误}iferr:=doBusiness(evt);err!=nil{iferrors.Is(err,ErrDBTimeout){return&TransientError{err}// 数据库超时,可重试}return&PermanentError{err}}returnnil}

第 3 类:重试次数耗尽的"真失败"

前面两类是"一眼就知道没救",这一类则是"试过了才知道没救"。

比如:调用下游支付接口,重试 5 次全部超时。这时候不能再重试了——继续重试只会加剧下游压力,而且大概率还是失败。

策略是:重试 N 次后进 DLQ,但 DLQ 必须带上下文(重试次数、每次的错误、原始消息),否则后面没法排查。

三、不该进 DLQ 的:瞬态失败

这是最容易犯的错:把"下游抖动"当成"永久失败"扔进 DLQ。

后果:一次 3 分钟的数据库抖动,几千条完全正常的消息被扔进死信队列,需要人工捞回来重放。这是自己给自己制造事故。

正确的做法是重试 + 退避,只有重试耗尽才进 DLQ。

判断标准很简单:这个失败,过一会儿再试会不会成功?

  • 会 → 重试;
  • 不会 → DLQ。

四、2 种重试策略

策略 1:延迟队列(分级重试)

RocketMQ 的做法最典型:重试消息进入不同延迟级别的队列。

第 1 次失败 → 10s 后重试 第 2 次 → 30s 第 3 次 → 1min 第 4 次 → 2min ... 第 16 次 → 2h 16 次全失败 → %DLQ%<group>

优点:实现简单,MQ 原生支持,而且重试消息不影响主 topic 的消费位点(它们在重试 topic 里)。

缺点:延迟级别是固定的,不能自定义;重试次数上限也是固定的。

策略 2:应用层重试 + 自建重试表

对于需要精细控制的场景(比如"支付类消息重试 5 次,通知类重试 3 次"),自建更灵活:

CREATETABLEretry_task(idBIGINTPRIMARYKEY,biz_idVARCHAR(64),payloadBLOB,attemptINTDEFAULT0,next_atDATETIME,-- 下次重试时间last_errorTEXT,statusTINYINT-- 0待重试 1成功 2进DLQ);
funcscheduleRetry(task*RetryTask){task.Attempt++iftask.Attempt>maxAttempts{task.Status=2// 进 DLQreturn}// 指数退避 + 抖动backoff:=time.Duration(1<<uint(task.Attempt))*time.Second jitter:=time.Duration(rand.Int63n(int64(backoff/2)))task.NextAt=time.Now().Add(backoff+jitter)task.Status=0}

优点:可以按业务类型定制策略、可以看到每次失败的原因、可以人工干预(改 status 重新投递)。

缺点:要自己实现,且重试任务的调度本身要有高可用保障。

两种策略怎么选

场景推荐
简单业务、失败即重试MQ 原生延迟重试
需要区分业务类型的重试策略自建重试表
需要看到失败原因做运营处理自建重试表
重试耗时要人工介入两者都要有,DLQ 是终点

五、DLQ 必须做的三件事

配置了 DLQ 但不管它,等于没配。三件事必须做:

1. 监控 DLQ 深度

# 告警规则 rabbitmq_queue_messages{queue=~".*DLQ.*"} > 0

DLQ 里有消息就应该是 P2 以上告警(除非你明确接受某些业务失败)。

2. DLQ 消息要带上下文

只存原始消息是不够的,后面排查时会问:

{"original":{...},"attempts":5,"errors":[{"at":"2026-10-13T00:01:00","err":"dial tcp 10.0.1.5:3306: i/o timeout"},{"at":"2026-10-13T00:01:30","err":"dial tcp 10.0.1.5:3306: i/o timeout"}],"consumerGroup":"order-consumer","firstFailedAt":"2026-10-13T00:01:00"}

有了errors数组,一眼就能看出"5 次全是数据库超时" → 那是下游故障,不是消息问题,可以批量重放。

3. 要有重放工具

DLQ 里的消息不是终点,要能捞回来重放:

# 从 DLQ 读出,写回主 topic# 关键:重放前先确认根因已修复,否则只是再失败一次

重放的前提是根因已经修好。没修就重放,等于把 DLQ 搬回主队列再失败一次。

六、带走物

消息类型处理
格式错误 / 无法解析直接 DLQ,不要重试(会阻塞主链路)
业务上不可能成功直接 DLQ+ 告警
瞬态失败(超时、503、限流)重试 + 退避,耗尽后才 DLQ
重试耗尽进 DLQ,但必须带 attempts + errors 上下文

配套三件事:监控 DLQ 深度并告警 / DLQ 消息带上下文 / 有重放工具且根因修好后再重放。

一句话结论:死信队列不是垃圾桶,它是"不可自动恢复消息的人工处理入口"——判断标准只有一条:再试一次会不会成功。会就重试,不会才进 DLQ。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 13:32:53

Python实现命令行计算器:从基础运算到工程实践

1. 项目概述&#xff1a;为什么选择开发Python计算器&#xff1f;计算器作为编程入门经典项目&#xff0c;看似简单却蕴含程序设计核心思想。我选择用Python实现这个项目&#xff0c;主要基于三点考量&#xff1a;首先&#xff0c;Python简洁的语法能清晰展现程序逻辑&#xff…

作者头像 李华
网站建设 2026/10/15 13:32:51

贝尔宾团队角色理论:九种角色如何解决团队协作问题

简介&#xff1a;这份PDF资料围绕贝尔宾团队角色理论展开&#xff0c;面向创业者、团队管理者及组织行为学学习者&#xff0c;帮助解决团队组建中角色配置与能力互补的问题。内容系统梳理了成功团队必备的九种角色&#xff0c;按行动导向、人际导向、谋略导向三大类划分&#x…

作者头像 李华
网站建设 2026/10/15 13:32:40

ASP AdRotator组件:广告轮播的核心原理与优化实践

1. ASP AdRotator组件概述在Web开发领域&#xff0c;广告轮播是商业网站的标配功能。ASP AdRotator组件作为微软经典服务器端广告管理解决方案&#xff0c;自ASP 3.0时代就已成为动态广告展示的首选工具。这个看似简单的组件实际上封装了广告调度、权重计算、点击统计等复杂逻辑…

作者头像 李华
网站建设 2026/10/15 13:32:40

LVS负载均衡核心原理与生产环境优化实践

1. LVS核心架构解析LVS&#xff08;Linux Virtual Server&#xff09;作为企业级负载均衡解决方案&#xff0c;其核心价值在于将多台真实服务器虚拟化为单一高性能服务单元。我在某电商平台流量治理项目中首次接触LVS时&#xff0c;发现其四层负载均衡性能可达到硬件设备的80%&…

作者头像 李华
网站建设 2026/10/15 13:32:35

从Word2Vec到World2Vec:词向量代码实现与工程实践

1. 从"Word2Vec"到"World2Vec"&#xff1a;这个笔误背后藏着什么第一次看到这个标题的时候&#xff0c;我盯着"World2Vec"看了好几秒。Word2Vec我太熟了&#xff0c;词向量领域的开山之作&#xff0c;做NLP的没人不知道。但World2Vec是什么&…

作者头像 李华
网站建设 2026/10/15 13:31:56

openrig:用Node.js和tmux统一管理Claude Code与Codex的本地环境

1. 从“openrig”说起&#xff1a;一个把 Claude Code 和 Codex 装进同一副骨架的思路第一次看到openrig这个词&#xff0c;我脑子里蹦出来的不是某个具体工具&#xff0c;而是一种“机架”的隐喻——rig 在英文里本来就有“装配、搭台子”的意思&#xff0c;open 则点明了它是…

作者头像 李华