消息积压与死信队列
❓ 面试官:如果线上突然发生了几百万条消息积压在 MQ 里,消费者根本消费不过来,你怎么快速处理这种紧急故障?
频率:🔥🔥🔥🔥🔥
💡 一句话总结(先抛结论): 千万不要傻等着它慢慢消费!核心的救火策略是:紧急扩容 + 临时消费者转发(Topic 拆分)。把原本积压在一个小水池里的水,光速抽到 10 个大水池里去,然后用 10 倍的算力去消耗。
📝 详细的“救火”实战三板斧(面试高分剧本):
第一步:排查原因,立刻止损
- 首先看监控大盘,为什么会突然积压?
- 如果是因为消费端代码出了 Bug(比如死循环或者数据库死锁导致消费极慢)。立刻修复代码,赶紧发版重启!
第二步:临时扩容(真正的核心杀招) 如果代码没 Bug,纯粹是突然来了 100 倍的流量(比如某顶流明星带货瞬间冲垮),此时增加几台原有的消费机器用处不大(因为原 Topic 的 Queue 数量是固定的,消费者数量超过 Queue 数量就会闲置)。 我们的操作:
- 写一个极简的“搬运工”消费者程序:这个程序不查数据库也不做业务,什么都不干。它只负责从原有的积压 Topic 里疯狂把消息拉出来。
- 新建一个扩容 10 倍的临时 Topic:比如原来只有 4 个 Queue,我们新建一个含有 40 个 Queue 的
Temp_Topic。 - 把那个“搬运工”拉出来的消息,原封不动地全速塞进这个
Temp_Topic里。(纯内存和网络操作,极快)。 - 紧急扩充 10 倍的业务消费机器:部署 40 台真正的业务消费者去专门监听这个
Temp_Topic,利用 10 倍的兵力快速消化积压。
第三步:善后工作
- 等积压的几百万消息全部消化完,把扩容的机器撤掉,恢复到原来的常规消费架构。这叫“大水漫灌后的收缩”。
❓ 面试官:如果有一条消息因为格式错误或者空指针,消费者一碰到它就报错,一直疯狂重试,导致整个消费端死循环卡住怎么办?(什么是死信队列?)【中高级】
频率:🔥🔥🔥🔥
💡 一句话总结(先抛结论): 成熟的 MQ 都有死信队列(Dead Letter Queue, DLQ)机制。如果一条消息经过了最大重试次数(比如 RocketMQ 默认 16 次,耗时约 2 小时)依然一直报错无法被消费成功,MQ 为了防止它永远占用队列资源,就会把它“流放”到一个专门的死信队列里去。
📝 详细原理解析与日常处理:
死信是怎么产生的?
- 消息被拒绝消费(Basic.Reject/Basic.Nack)且没有放回原队列。
- 消息在队列里存活时间过期了(TTL 到了,通常用于实现延迟队列)。
- 消息超过了最大重试次数(最常见的业务代码 Bug 导致)。
死信队列在哪儿?
- 在 RocketMQ 中,死信队列会以
%DLQ%消费者组名的特殊命名自动被创建出来。它本质上依然是一个普通的 Topic 队列。
- 在 RocketMQ 中,死信队列会以
我们怎么处理死信?(实战加分项)
- "面试官您好,产生死信说明我们的业务代码碰到了无法处理的『脏数据/极端边缘 Case』。如果不去管它,这笔订单就永远卡死了。"
- 实战做法:我们会单独写一个极其简单的预警微服务,专门去订阅所有的
%DLQ%开头的死信队列。一旦有死信进来,我们就发一条飞书/钉钉告警给开发人员。 - 然后开发人员去后台管理系统里查看这条死信的 JSON 报文,排查出是哪一行字段格式传错了。通过手工修补数据或者修复 Bug 后,在后台点击“重新投递”,把它再发回正常队列里被消费一次。
❓ 面试官:我看到你们在项目中用 MQ 实现了“订单超过 30 分钟未支付自动取消”,这是怎么做的?(延迟队列机制)【高级】
频率:🔥🔥🔥
💡 一句话总结(先抛结论): 我们在订单创建成功后,发送一条延迟时间为 30 分钟的延迟消息。MQ 收下这条消息后,会把它先藏起来,等 30 分钟时间一到,再把它投递给正常的业务队列。消费者拿到了就知道:“哦,这个订单已经满 30 分钟了,我去查下它付没付款”。
📝 不同 MQ 的底层实现对比(极其重要):
RocketMQ(最简单好用,自带支持):
- 商业版支持任意精度延迟。
- 开源版支持 18 个固定级别的延迟(如 1s, 5s, ..., 30m, 2h)。发消息时只需
msg.setDelayTimeLevel(16)即可。 - 底层原理:当你发延迟消息时,RocketMQ 会悄悄把它拦截下来,发到一个名为
SCHEDULE_TOPIC_XXXX的内部系统 Topic 里。后台有个定时任务线程池每秒去扫描这个系统 Topic。发现时间到了,再把它拎出来,恢复它原本的 Topic 名字,重新发到消费者能看见的普通队列里。
RabbitMQ(本身不支持,需要插件或死信绕弯路):
- 很多老项目如果用 RabbitMQ,会利用死信队列(DLQ)+ 消息过期时间(TTL)的组合技来变相实现。
- 玩法:创建一个没有消费者的
队列A,设置它的消息存活时间 TTL 为 30 分钟。绑定一个死信队列B。把订单消息发给队列A,因为它没消费者,30分钟后消息“老死”,就会被转入到队列B。我们专门写代码去消费队列B即可。
🌟 为什么不用定时任务做(如 XXL-JOB 每分钟扫一次数据库)?
- 数据库压力极大:如果要精确到秒级,定时任务每一秒都要去对庞大的订单表执行一次全表范围扫描。
- 精度不够且容易漏:如果是每 5 分钟扫一次,那个 30 分钟临界点的订单会被严重拖延取消。而 MQ 延迟消息可以把压力完全分摊在时间流里,是最高效优雅的解法。