[TOC] #### 1. 前言 --- 上一讲我们用 TTL 实现了订单 30 分钟超时,但留了一个问题:消息过期之后去哪了 ? 答案是:如果你没配置死信队列,过期消息就直接消失了 那么需要思考一下,什么是死信 ? 死信就是 “死了的消息”,以下几种情况,消息会变成死信: + 消息过期了:TTL 到了,还没被消费 + 消费者拒收:消费者调用了 `reject` 或 `nack`,且不重新入队 + 队列满了:队列设置了最大长度,新消息进不来 这些消息不能直接扔掉,因为你可能还需要处理它们,比如订单超时的消息,过期了说明用户没付款,需要触发取消逻辑 #### 2. 死信队列 --- 什么是死信队列(DLX)? DLX 是 `Dead Letter Exchange` 的缩写,它是一个普通的 Exchange,专门用来接收死信 当消息变成死信的时候,RabbitMQ 会自动把它转发到 DLX 绑定的队列,你只需要配置一下,不需要写额外的代码 上一讲我们说 TTL 消息过期后会消失,现在加上 DLX,流程就完整了: ```plaintext 用户下单 → 消息进入 order_queue(TTL 30分钟) ↓ 30分钟后消息过期 ↓ 消息变成死信,转发到 DLX ↓ DLX 路由到 timeout_queue ↓ 消费者从 timeout_queue 取消息,取消订单 ``` #### 3. 代码实现 --- 第一步:声明死信 Exchange 和死信队列 ```php // 死信 Exchange(用 Direct 类型) $channel->exchange_declare('dlx_exchange', 'direct', false, true, false); // 死信队列 $channel->queue_declare('timeout_queue', false, true, false, false); $channel->queue_bind('timeout_queue', 'dlx_exchange', 'order_timeout'); ``` 第二步:声明业务队列,绑定 DLX ```php $args = new AMQPTable([ 'x-message-ttl' => 1800000, // 30分钟过期 'x-dead-letter-exchange' => 'dlx_exchange', // 过期后发到这个 Exchange 'x-dead-letter-routing-key' => 'order_timeout', // 路由标识 ]); $channel->queue_declare('order_queue', false, true, false, false, false, $args); ``` 关键配置就两个: + x-dead-letter-exchange:死信发到哪个 Exchange + x-dead-letter-routing-key:用什么路由标识 第三步:用户下单时发消息,消息进入 `order_queue`,开始 30 分钟倒计时 ```php $message = new AMQPMessage(json_encode([ 'order_id' => $order->id, ])); $channel->basic_publish($message, '', 'order_queue'); ``` 第四步:消费者从死信队列取消息 ```php $callback = function ($msg) { $data = json_decode($msg->body, true); $order = DB::table('orders')->find($data['order_id']); if ($order && $order->status === 'unpaid') { cancelOrder($order->id); echo "订单 {$order->id} 超时取消\n"; } $msg->ack(); }; $channel->basic_consume('timeout_queue', '', false, false, false, false, $callback); ``` 完整流程 + 用户下单,消息进入 `order_queue`,TTL 30 分钟 + 用户在 30 分钟内付款了 → 消息被正常消费,什么都不发生(当前还没有实现) + 用户没付款 → 30 分钟后消息过期 → 自动转发到 dlx_exchange → 进入 timeout_queue → 消费者取消订单 不需要定时任务,不需要扫表,RabbitMQ 自己计时,自动触发 #### 4. 完整示例 --- 生产者: ```php require_once __DIR__ . '/vendor/autoload.php'; use PhpAmqpLib\Connection\AMQPStreamConnection; use PhpAmqpLib\Message\AMQPMessage; use PhpAmqpLib\Wire\AMQPTable; $connection = new AMQPStreamConnection('192.168.3.20', 5672, 'admin', 'admin123'); $channel = $connection->channel(); # 第一步:声明死信 Exchange 和死信队列 // 死信 Exchange(用 Direct 类型) $channel->exchange_declare('dlx_exchange', 'direct', false, true, false); // 死信队列 $channel->queue_declare('timeout_queue', false, true, false, false); $channel->queue_bind('timeout_queue', 'dlx_exchange', 'order_timeout'); # 第二步:声明业务队列,绑定 DLX $args = new AMQPTable([ 'x-message-ttl' => 10000, // 10秒后过期(方便测试) 'x-dead-letter-exchange' => 'dlx_exchange', // 过期后发到这个 Exchange 'x-dead-letter-routing-key' => 'order_timeout', // 路由标识 ]); $channel->queue_declare('order_queue', false, true, false, false, false, $args); # 第三步:用户下单时发消息,消息进入 order_queue,开始倒计时 $order_id = mt_rand(100000, 999999); $message = new AMQPMessage(json_encode(['order_id' => $order_id])); $channel->basic_publish($message, '', 'order_queue'); echo "消息已发送 {$order_id}\n"; $channel->close(); $connection->close(); ``` 消费者: ```php require_once __DIR__ . '/vendor/autoload.php'; use PhpAmqpLib\Connection\AMQPStreamConnection; $connection = new AMQPStreamConnection('192.168.3.20', 5672, 'admin', 'admin123'); $channel = $connection->channel(); $channel->exchange_declare('dlx_exchange', 'direct', false, true, false); $channel->queue_declare('timeout_queue', false, true, false, false); $channel->queue_bind('timeout_queue', 'dlx_exchange', 'order_timeout'); echo "等待消息中...\n"; # 第四步:消费者从死信队列取消息 $callback = function ($msg) { // $data = json_decode($msg->body, true); // $order = DB::table('orders')->find($data['order_id']); // if ($order && $order->status === 'unpaid') { // cancelOrder($order->id); // echo "订单 {$order->id} 超时取消\n"; // } echo "订单超时取消: {$msg->body}\n"; $msg->ack(); }; $channel->basic_consume('timeout_queue', '', false, false, false, false, $callback); while ($channel->is_consuming()) { $channel->wait(); } ``` #### 5. 本文小结 --- 死信队列就是给「死了的消息」一个去处,消息变成死信的三种情况: + 消息过期(TTL) + 消费者拒收 + 队列满了 配置 DLX 只需要在业务队列上加两个参数: + x-dead-letter-exchange + x-dead-letter-routing-key TTL + DLX 是一对经典组合,能实现延迟任务、超时取消等功能,下一讲我们来聊聊延迟队列的更多实现方案