rabbitmq使用场景之(二) 死信队列
案例说明
本案例主要为实现“死信队列”
模拟的是一个用户下单后,需在规定时间内支付(此处设置为了3s),否则取消订单的场景
其中设置了队列的一些参数,例如x-max-length该队列最多能存储1000条消息,x-message-ttl过期时间为3s等等
主要逻辑是:
- 给订单队列设置死信队列参数,这样当订单超时时将自动触发死信参数设置
- 声明死信交换机、声明队列,并通过 死信路由键 将 死信交换机 与死信队列 形成绑定关系(这个过程中如果死信队列、死信交换机、死信路由键等不存在则会自动创建相关
- 3s时间一到消息将通过 死信交换机 转发到 死信队列上
- 此时开启死信消费者,触发dead_receive()逻辑,最后触发callback函数(自定义的函数名称,也可以叫其他的名字,不影响业务),callback中做订单超时是否支付、支付超时后的取消下单逻辑
- 在这个过程中,
订单队列是不能有消费者的,只能为死信队列开启消费者(因为消费了就无法走入死信队列 - 死信队列名称可为任意名称,此处为方便理解,特意写了dead字眼
架构图

效果图
交换机

队列

命令消费

Index控制器
框架用的thinkphp6
为了简单,生产者和消费者都写在一个控制器里
死信队列名就叫dead
<?php
namespace app\controller;
use app\BaseController;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
use PhpAmqpLib\Wire\AMQPTable;
class Index extends BaseController
{
// 队列和交换机常量
const ORDER_QUEUE = 'order_queue';
const DEAD_QUEUE = 'dead_queue';
const DEAD_EXCHANGE = 'dead_exchange';
const DEAD_ROUTING_KEY = 'dead_routing_key';
// 单例连接
public static $connection ;
// 模拟订单生成
public function index()
{
// 生成订单号
$chars = md5(uniqid(mt_rand(), true));
$uuid = substr ( $chars, 0, 8 ) . '-'
. substr ( $chars, 8, 4 ) . '-'
. substr ( $chars, 12, 4 ) . '-'
. substr ( $chars, 16, 4 ) . '-'
. substr ( $chars, 20, 12 );
$data = [
'order_id' => $uuid, // 订单号
'pay_status' => 'pending', // 初始化支付状态为 pending
];
// 设置缓存,后续死信队列中读取支付状态
cache('order_info_'.$uuid, $data, 3600);
$this->send(json_encode( cache('order_info_'.$uuid) ));
$this->simulatedPayment('order_info_'.$uuid);
return '订单已生成';
}
// 模拟订单支付支付状态
public function simulatedPayment($order_id)
{
$data = cache($order_id);
$data['pay_status'] = mt_rand(0,1) ? 'confirmed' : 'pending';
cache($order_id, $data, 3600);
}
// 生产者发送消息
public function send(string $data)
{
list($channel, $connection) = self::getChanel();
$arguments = new AMQPTable([
'x-max-length' => 1000, // 设置队列存储上限1000条消息
'x-dead-letter-exchange' => self::DEAD_EXCHANGE, // 绑定死信交换机
'x-message-ttl' => 3 * 1000, // 设置队列消息过期时间为3秒
'x-dead-letter-routing-key' => self::DEAD_ROUTING_KEY // 绑定死信路由键
]);
$channel->queue_declare(self::ORDER_QUEUE, false, false, false, false, false, $arguments); // 声明订单队列,并为该队列配置上述死信参数
$properties = ['content_type' => 'application/json', 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT];
$msg = new AMQPMessage($data, $properties); // 将数据实例化成消息实例(后续简称为消息)
$channel->basic_publish($msg, '', self::ORDER_QUEUE, false, false, null); // 将该消息投递到order队列中
$this->setupDeadLetterQueue($channel);
$channel->close();
$connection->close();
}
// 配置死信队列
public function setupDeadLetterQueue($channel)
{
/*
第二参:交换器类型
direct: (默认)直接交换器,工作方式类似于单播,Exchange会将消息发送完全匹配ROUTING_KEY的Queue,
fanout: 广播式交换器,不管消息的ROUTING_KEY设置为什么,Exchange都会将消息转发给所有绑定的Queue,
topic: 主题交换器,工作方式类似于组播,Exchange会将消息转发和ROUTING_KEY匹配模式相同的所有队列,比如,ROUTING_KEY为user.stock的Message会转发给绑定匹配模式为 * .stock,user.stock, * . * 和#.user.stock.#的队列。(* 表是匹配一个任意词组,#表示匹配0个或多个词组),
headers:根据消息体的header匹配
*/
$channel->exchange_declare(self::DEAD_EXCHANGE, 'direct', false, false, false); // 声明死信交换机
$channel->queue_declare(self::DEAD_QUEUE, false, false, false, false); // 声明死信队列
$channel->queue_bind(self::DEAD_QUEUE, self::DEAD_EXCHANGE, self::DEAD_ROUTING_KEY); // 将死信队列 通过死信路由键 绑定到死信交换机
}
// 死信队列消费者
public static function dead_receive()
{
list($channel, $connection) = self::getChanel();
$channel->basic_qos(null, 1, null); // 限制消费能力,每次只消费1条
$channel->basic_consume(self::DEAD_QUEUE, '', false, false, false, false, [self::class, 'callback']); // 开始消费死信队列中的消息,通过自定义的callback回调函数处理死信消费逻辑
while ($channel->is_open()) {
$channel->wait();
}
}
// 死信队列消费处理
public static function callback($msg)
{
// 此时该订单已超过规定的支付时间(x-message-ttl),进入此死信队列逻辑
$content = json_decode($msg->body, true);
// 拿到订单id,读取缓存中的支付状态,用于判断是否支付(这里用读取缓存,代替业务中根据订单号查询支付状态这步)
$data = cache('order_info_' . $content['order_id']);
// 只有未支付的订单才做订单取消逻辑
if('confirmed'!= $data['pay_status']){
// 这里可以添加订单取消 或 通知逻辑
echo "订单".$data['order_id'] . " 已失效,请重新下单!\n";
}
$msg->ack();
}
// 创建信道和连接
public static function getChanel()
{
$config = [
'host' => 'localhost',
'port' => 5672,
'user' => 'your username',
'password' => 'your password',
];
extract($config);
if(is_null(self::$connection)){
self::$connection = new AMQPStreamConnection($host, $port, $user, $password);
}
$channel = self::$connection->channel();
return [$channel, self::$connection];
}
}
dead指令
<?php
declare (strict_types = 1);
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
// 引入控制器
use app\controller\Index;
class Dead extends Command
{
protected function configure()
{
// 指令配置
$this->setName('dead')
->setDescription('死信队列消费指令');
}
protected function execute(Input $input, Output $output)
{
// 执行死信消费逻辑
Index::dead_receive();
}
}

浙公网安备 33010602011771号