rabbitmq使用场景之(二) 死信队列

案例说明

本案例主要为实现“死信队列
模拟的是一个用户下单后,需在规定时间内支付(此处设置为了3s),否则取消订单的场景
其中设置了队列的一些参数,例如x-max-length该队列最多能存储1000条消息,x-message-ttl过期时间为3s等等

主要逻辑是:

  1. 给订单队列设置死信队列参数,这样当订单超时时将自动触发死信参数设置
  2. 声明死信交换机、声明队列,并通过 死信路由键 将 死信交换机 与死信队列 形成绑定关系(这个过程中如果死信队列、死信交换机、死信路由键等不存在则会自动创建相关
  3. 3s时间一到消息将通过 死信交换机 转发到 死信队列上
  4. 此时开启死信消费者,触发dead_receive()逻辑,最后触发callback函数(自定义的函数名称,也可以叫其他的名字,不影响业务),callback中做订单超时是否支付、支付超时后的取消下单逻辑
  5. 在这个过程中,订单队列是不能有消费者的,只能为死信队列开启消费者(因为消费了就无法走入死信队列
  6. 死信队列名称可为任意名称,此处为方便理解,特意写了dead字眼

架构图

image

效果图

交换机
image


队列
image


命令消费
image

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();
    }
}

参考链接

RabbitMQ+PHP php-amqplib使用教程与常用场景-死信队列等

posted @ 2024-06-12 10:41  Anbin啊  阅读(66)  评论(0)    收藏  举报