前几天排查对账单,发现有十几笔第三方支付回调的订单状态死活对不上。翻了半天消费端日志,直接让人高血压:原来是负责推送订单的同学在消费端写了个极为粗暴的 try-catch。只要消费失败,他在 catch 块里打印了一条无关痛痒的 error 日志,接着就直接给 RabbitMQ 返回了 Ack!
这种直接丢弃(Discard)数据的处理方式,在分布式高并发系统里简直是灾难。数据库事务失败、第三方 API 抖动、网络瞬时故障,任何一个因素都能让你的异步任务死机。如果直接扔掉,排查困难不说,数据一致性还彻底崩盘。
如果不丢弃,直接 Nack(requeue=true) 回滚到原队列?那更恐怖。如果是一个由于代码 Bug 导致 100% 报错的毒丸消息(Poison Message),队列会以每秒上万次的频率死循环重试,瞬间把 CPU 烧干,整个队列的正常消费全部被堵死。
为了优雅地搞定这个问题,我们利用 RabbitMQ 的**死信交换机(DLX,Dead Letter Exchange)**构建了一套渐进式延迟重试与补偿机制。
其原理非常巧妙:
- 工作队列(Work Queue):接收业务消息,消费者在这里消费。
- 死信退避队列(Retry Queue):当工作队列消费失败时,消费者记录已重试次数,如果未超限,则将消息发送至退避队列,并根据重试次数设置不同的过期时间(TTL)。该队列不设消费者。
- 退避队列的 DLX:退避队列配置了
x-dead-letter-exchange属性指向工作队列的交换机。当 TTL 到期后,消息会自动变成“死信”,通过绑定的路由键重新投递回工作队列重新消费。 - 最终死信队列(DLQ):重试超过最大次数(如 5 次)的消息,直接路由到人工干预的 DLQ,发钉钉/企业微信告警,等待手动补偿。
以下是基于 Node.js amqplib 完整落地的队列配置与延迟重试核心代码:
import * as amqp from 'amqplib';
const RABBITMQ_URL = 'amqp://admin:admin123@localhost:5672';
const MAIN_EXCHANGE = 'order.exchange';
const WORK_QUEUE = 'order.work.queue';
const RETRY_EXCHANGE = 'order.retry.exchange';
const RETRY_QUEUE = 'order.retry.10s.queue'; // 10秒延迟重试队列
const DLQ_QUEUE = 'order.dlq.queue';
async function setupRabbitMQ() {
const connection = await amqp.connect(RABBITMQ_URL);
const channel = await connection.createChannel();
// 1. 声明主业务交换机与队列
await channel.assertExchange(MAIN_EXCHANGE, 'topic', { durable: true });
await channel.assertQueue(WORK_QUEUE, {
durable: true,
arguments: {
// 队列溢出或被拒的死信直接进入 DLQ
'x-dead-letter-exchange': MAIN_EXCHANGE,
'x-dead-letter-routing-key': 'order.dlq',
}
});
await channel.bindQueue(WORK_QUEUE, MAIN_EXCHANGE, 'order.payment.#');
// 2. 声明延迟重试队列(核心:利用 TTL 和 DLX 实现延迟重发)
await channel.assertExchange(RETRY_EXCHANGE, 'direct', { durable: true });
await channel.assertQueue(RETRY_QUEUE, {
durable: true,
arguments: {
// 消息 TTL 到期后,自动作为死信发回主交换机
'x-dead-letter-exchange': MAIN_EXCHANGE,
'x-dead-letter-routing-key': 'order.payment.retry',
// 设置固定过期时间 10000 毫秒(10秒)
'x-message-ttl': 10000,
}
});
await channel.bindQueue(RETRY_QUEUE, RETRY_EXCHANGE, 'retry.10s');
// 3. 声明最终死信队列 (DLQ) 用于人工排查
await channel.assertQueue(DLQ_QUEUE, { durable: true });
await channel.bindQueue(DLQ_QUEUE, MAIN_EXCHANGE, 'order.dlq');
// 4. 启动消费者
channel.consume(WORK_QUEUE, async (msg) => {
if (!msg) return;
const content = JSON.parse(msg.content.toString());
const headers = msg.properties.headers || {};
const retryCount = (headers['x-retry-count'] as number) || 0;
try {
console.log(`[Consumer] 正在处理支付回调消息: ${content.orderId}, 当前重试次数: ${retryCount}`);
// 模拟偶尔发生的外部接口调用失败
if (Math.random() > 0.1) {
throw new Error('Connect gateway timeout');
}
console.log(`[Consumer] 订单 ${content.orderId} 推送成功!`);
channel.ack(msg);
} catch (err: any) {
console.error(`[Consumer] 处理订单 ${content.orderId} 出错: ${err.message}`);
if (retryCount < 3) {
// 未超限,发布到延迟重试队列
const nextRetryCount = retryCount + 1;
channel.ack(msg); // 确认原消息,防止堆积
channel.publish(RETRY_EXCHANGE, 'retry.10s', msg.content, {
persistent: true,
headers: {
'x-retry-count': nextRetryCount,
}
});
console.log(`[Retry] 已将订单 ${content.orderId} 投递至重试队列,等待 10s 后重试`);
} else {
// 超过重试上限,直接送入 DLQ
console.error(`[Alert] 订单 ${content.orderId} 重试次数超限,直接丢入 DLQ 进行排查监控!`);
channel.reject(msg, false); // false 表示不放入原队列,直接走绑定的 DLX
}
}
});
}
setupRabbitMQ().catch(console.error);
有了这套架构,支付微服务再也不怕上游偶发性的网络抖动。出错的消息会在退避队列挂机 10 秒钟,然后神不知鬼不觉地被吐回工作队列,大部分偶发性故障都在第二轮重试中被完美搞定。
你们在 RabbitMQ 里做延迟队列时,是利用我们这种原生的 TTL + DLX 双向循环路由,还是干脆给 RabbitMQ 安装官方的 rabbitmq_delayed_message_exchange 插件来实现?