前言

相关概念

消息(Message)

是指在应用间传送的数据。消息可以非常简单,比如只包含文本字符串,也可以更复杂,可能包含嵌入对象。

消息队列(Message Queue)

是一种应用间的通信方式,消息发送后可以立即返回,由消息系统来确保消息的可靠传递。消息发布者只管把消息发布到 MQ 中而不用管谁来取,消息使用者只管从 MQ 中取消息而不管是谁发布的。这样发布者和使用者都不用知道对方的存在。

RabbitMQ

RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。

AMQP

Advanced Message Queue,高级消息队列协议。

Erlang

面向并发的编程语言。

RabbitMQ

特点

1.可靠性(Reliability) RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认。

2.灵活的路由(Flexible Routing) 在消息进入队列之前,通过 Exchange 来路由消息的。对于典型的路由功能,RabbitMQ已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。

3.消息集群(Clustering) 多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker

4.高可用(Highly Available Queues) 队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。

5.多种协议(Multi-protocol) RabbitMQ 支持多种消息队列协议,比如 STOMP、MQTT 等等。

6.多语言客户端(Many Clients) RabbitMQ 几乎支持所有常用语言,比如 Java、.NET、Ruby 等等。

7.管理界面(Management UI) RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker 的许多方面。

8.跟踪机制(Tracing) 如果消息异常,RabbitMQ 提供了消息跟踪机制,使用者可以找出发生了什么。

9.插件机制(Plugin System) RabbitMQ 提供了许多插件,来从多方面进行扩展,也可以编写自己的插件。

概念模型
图片

图片

  • Message
    消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
  • Publisher
    消息的生产者,也是一个向交换器发布消息的客户端应用程序。
  • Exchange
    交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。

    • Direct Exchange:直连交换机,根据Routing Key(路由键)进行投递到不同队列。
    • Fanout Exchange:扇形交换机,采用广播模式,根据绑定的交换机,路由到与之对应的所有队列。
    • Topic Exchange:主题交换机,对路由键进行模式匹配后进行投递,符号#表示一个或多个词,*表示一个词。
    • Header Exchange:头交换机,不处理路由键。而是根据发送的消息内容中的headers属性进行匹配。
  • Binding
    绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。

    $queue->bind($exchange->getName(), $routeKey);
  • Queue
    消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
  • Connection
    网络连接,比如一个TCP连接。
  • Channel
    信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
  • Consumer
    消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
  • Virtual Host
    虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / 。
  • Broker
    表示消息队列服务器实体。

安装

Windows环境

安装Erlang

下载地址: https://erlang.org/download/otp_win64_25.0.exe

配置环境变量 ERLANG_HOME C:\Program Files (x86)\erl5.9

添加到PATH %ERLANG_HOME%\bin;

安装RabbitMq

下载地址:https://www.rabbitmq.com/百度网盘

配置环境变量 C:\Program Files (x86)\RabbitMQ Server\rabbitmq_server-3.9.11

添加到PATH %RABBITMQ_SERVER%\sbin;

运行:

D:\Program Files\RabbitMQ Server\rabbitmq_server-3.9.11\sbin>rabbitmq-server.bat

访问:

打开浏览器。访问 http://127.0.0.1:15672

注:

  1. 默认账号:guest 密码:guest,仅用于浏览器访问,API需要新建用户
  2. 默认浏览器访问端口15672,API访问端口5672

安装php的amqp扩展

1.下载地址:http://pecl.php.net/package/amqp

2.将php_amqp.dll复制到php/ext,同时在php.ini中添加如下代码:

[amqp]  
extension=php_amqp.dll 

3.然后将rabbitmq.4.dll复制到php根目录

4.查看是否安装成功:php -m

Docker环境

安装RabbitMq

docker pull rabbitmq # 镜像未配有控制台
docker pull rabbitmq:management # 镜像配有控制台
docker run --name rabbitmq -d -p 15672:15672 -p 5672:5672 rabbitmq:management

启动容器后,可以浏览器中访问http://localhost:15672来查看控制台信息。
RabbitMQ默认的用户名:guest,密码:guest

安装php和amqp扩展

docker pull php:7.4-fpm
docker run -d -p 9000:9000 -v /Users/ma/docker/php:/www --name phpfpm php:7.4-fpm
docker exec -it phpfpm /bin/bash

#安装扩展
apt-get update && apt-get install -y libfreetype6-dev librabbitmq-dev libjpeg62-turbo-dev libmcrypt-dev libpng-dev
pecl install amqp
docker-php-ext-enable amqp

使用

普通队列

生产者
$params = [
    'host' => '192.168.0.134',
    'port' => '5672',
    'vhost' => '/',
    'login' => 'admin',
    'password' => '123456'
];
$connection = new \AMQPConnection($params);
if (!$connection->connect()) {
    echo "Cannot connect to the broker!";
    exit;
}
$channel = new \AMQPChannel($connection);
$exchangeName = 'direct_exchange';
$queueName = 'direct_queue';
$routeKey = 'direct_queue';
$exchange = new \AMQPExchange($channel);
$exchange->setName($exchangeName);
$exchange->setFlags(AMQP_DURABLE);
$exchange->setType(AMQP_EX_TYPE_DIRECT);
// 声明交换机
$exchange->declareExchange();
// 创建消息队列
$queue = new \AMQPQueue($channel);
$queue->setName($queueName);
// 设置持久性
$queue->setFlags(AMQP_DURABLE);
// 声明消息队列
$queue->declareQueue();
// 开启事务,确保数据真正不丢失
$channel->startTransaction();
// 将消息和标识绑定到交换器中
$exchange->publish($message, $routeKey);
$channel->commitTransaction();
$connection->disconnect();
var_dump("[x] Sent $message");
消费者
$params = [
    'host' => '192.168.0.134',
    'port' => '5672',
    'vhost' => '/',
    'login' => 'admin',
    'password' => '123456'
];
$connection = new \AMQPConnection($params);
if (!$connection->connect()) {
    echo "Cannot connect to the broker!";
    exit;
}
$channel = new \AMQPChannel($connection);
$exchangeName = 'direct_exchange';
$queueName = 'direct_queue';
$routeKey = 'direct_queue';
$exchange = new \AMQPExchange($channel);
$exchange->setName($exchangeName);
$exchange->setType(AMQP_EX_TYPE_DIRECT);
$exchange->declareExchange();
$queue = new \AMQPQueue($channel);
$queue->setName($queueName);
$queue->setFlags(AMQP_DURABLE);
$queue->declareQueue();
$queue->bind($exchange->getName(), $routeKey);
// 接收消息并处理回调
$queue->consume(function ($envelop, $queue) {
    $message = $envelop->getBody();
    echo $message . PHP_EOL;
    // ACK 通知生产者任务完成
    $queue->ack($envelop->getDeliveryTag(), AMQP_NOPARAM);
});
//设置每次只能处理一条,避免消息堆积,从而导致队列挂掉
$channel->qos(0, 1);
//关闭连接
$connection->disconnect();

延迟队列

生产者
<?php
//来源公众号:【码农编程进阶笔记】
//header('Content-Type:text/html;charset=utf-8;');
 
$params = array(
    'exchangeName' => 'test_cache_exchange',
    'queueName' => 'test_cache_queue',
    'routeKey' => 'test_cache_route',
);
 
$connectConfig = array(
    'host' => 'localhost',
    'port' => 5672,
    'login' => 'guest',
    'password' => 'guest',
    'vhost' => '/'
);
 
//var_dump(extension_loaded('amqp')); 判断是否加载amqp扩展
//exit();
for($i=5;$i>0;$i--){
 
    try {
        $conn = new AMQPConnection($connectConfig);
        $conn->connect();
        if (!$conn->isConnected()) {
            //die('Conexiune esuata');
            //TODO 记录日志
            echo 'rabbit-mq 连接错误:', json_encode($connectConfig);
            exit();
        }
        $channel = new AMQPChannel($conn);
        if (!$channel->isConnected()) {
            // die('Connection through channel failed');
            //TODO 记录日志
            echo 'rabbit-mq Connection through channel failed:', json_encode($connectConfig);
            exit();
        }
        $exchange = new AMQPExchange($channel);
        $exchange->setFlags(AMQP_DURABLE);//持久化
        $exchange->setName($params['exchangeName']);
        $exchange->setType(AMQP_EX_TYPE_DIRECT); //direct类型
        $exchange->declareExchange();
 
        //$channel->startTransaction();
        //RabbitMQ不容许声明2个相同名称、配置不同的Queue队列
        $queue = new AMQPQueue($channel);
        $queue->setName($params['queueName'].$i);
        $queue->setFlags(AMQP_DURABLE);
        $queue->setArguments(array(
            'x-dead-letter-exchange' => 'delay_exchange',        死信交换机
            'x-dead-letter-routing-key' => 'delay_route',          // 死信路由
            'x-message-ttl' => (10000*$i),       // 当上面的消息扔到该队列中后,过了60秒,如果没有被消费,它就死了
            // 在RMQ中想要使用优先级特性需要的版本为3.5+。
            //'x-max-priority'=>0,//将队列声明为优先级队列,即在创建队列的时候添加参数 x-max-priority 以指定最大的优先级,值为0-255(整数)。
        ));
        $queue->declareQueue();
 
        //绑定队列和交换机
        $queue->bind($params['exchangeName'], $params['routeKey'].$i);
        //$channel->commitTransaction();
    } catch(Exception $e) {
 
    }
 
    // 当mandatory标志位设置为true时,如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用basic.return方法将消息返还给生产者;当mandatory设为false时,出现上述情形broker会直接将消息扔掉。
    //delivery_mode=2指明message为持久的
    //生成消息
    echo '发送时间:'.date("Y-m-d H:i:s", time()).PHP_EOL;
    echo 'i='.$i.',延迟'.($i*10).'秒'.PHP_EOL;
    $message = json_encode(['order_id'=>time(),'i'=>$i]);
    $exchange->publish($message, $params['routeKey'].$i, AMQP_MANDATORY, array('delivery_mode'=>2));
    $conn->disconnect();
    sleep(2);
}
消费者
<?php
//来源公众号:【码农编程进阶笔记】
//header('Content-Type:text/html;charset=utf8;');
 
$params = array(
    'exchangeName' => 'delay_exchange',
    'queueName' => 'delay_queue',
    'routeKey' => 'delay_route',
);
 
$connectConfig = array(
    'host' => 'localhost',
    'port' => 5672,
    'login' => 'guest',
    'password' => 'guest',
    'vhost' => '/'
);
 
//var_dump(extension_loaded('amqp'));
try {
    $conn = new AMQPConnection($connectConfig);
    $conn->connect();
    if (!$conn->isConnected()) {
        //die('Conexiune esuata');
        //TODO 记录日志
        echo 'rabbit-mq 连接错误:', json_encode($connectConfig);
        exit();
    }
    $channel = new AMQPChannel($conn);
    if (!$channel->isConnected()) {
        // die('Connection through channel failed');
        //TODO 记录日志
        echo 'rabbit-mq Connection through channel failed:', json_encode($connectConfig);
        exit();
    }
    $exchange = new AMQPExchange($channel);
    $exchange->setFlags(AMQP_DURABLE);//声明一个已存在的交换器的,如果不存在将抛出异常,这个一般用在consume端
    $exchange->setName($params['exchangeName']?:'');
    $exchange->setType(AMQP_EX_TYPE_DIRECT); //direct类型
    $exchange->declareExchange();
 
    //$channel->startTransaction();
 
    $queue = new AMQPQueue($channel);
    $queue->setName($params['queueName']?:'');
    $queue->setFlags(AMQP_DURABLE);
    $queue->declareQueue();
 
    //绑定
    $queue->bind($params['exchangeName'], $params['routeKey']);
} catch(Exception $e) {
    echo $e->getMessage();
    exit();
}
 
function callback(AMQPEnvelope $message) {
    global $queue;
    if ($message) {
        $body = $message->getBody();
        echo '接收时间:'.date("Y-m-d H:i:s", time()). PHP_EOL;
        echo '接收内容:'.$body . PHP_EOL;
        //为了防止接收端在处理消息时down掉,只有在消息处理完成后才发送ack消息
        $queue->ack($message->getDeliveryTag());
    } else {
        echo 'no message' . PHP_EOL;
    }
}
 
//$queue->consume('callback');  第一种消费方式,但是会阻塞,程序一直会卡在此处
 
//注意:这里需要注意的是这个方法:$queue->consume,queue对象有两个方法可用于取消息:consume和get。前者是阻塞的,无消息时会被挂起,适合循环中使用;后者则是非阻塞的,取消息时有则取,无则返回false。
//就是说用了consume之后,会同步阻塞,该程序常驻内存,不能用nginx,apache调用。 
 
$action = '2';
if($action == '1'){
    $queue->consume('callback');  //第一种消费方式,但是会阻塞,程序一直会卡在此处
}else{
    //第二种消费方式,非阻塞
    $start = time();
    while(true)
    {
        $message = $queue->get();
        if(!empty($message))
        {
            echo '接收时间:'.date("Y-m-d H:i:s", time()). PHP_EOL;
            echo '接收内容:'.$message->getBody().PHP_EOL;
            $queue->ack($message->getDeliveryTag());    //应答,代表该消息已经消费
            $end = time();
            echo '运行时间:'.($end - $start).'秒'.PHP_EOL;
            //exit();
        }
        else
        {
            //echo 'message not found' . PHP_EOL;
        }
    }
}

守护进程

Windows端:supervisor-win

下载地址:https://pypi.org/project/supervisor-win/

安装

#重装setuptools
sudo pip3 uninstall setuptools
pip3 install setuptools --upgrade

#安装supervisor-win
pip install supervisor-win

配置

app/public/supervisord/supervisord.conf

[program:cancelUnpayUniOrder]
directory=E:\\dev\\tp51\\app
command=D:\\phpstudy_pro\\Extensions\\php\\php7.3.4nts\\php.exe think cancelUnpayUniOrder

[program:syncWechatPayResult]
directory=E:\\dev\\tp51\\app
command=D:\\phpstudy_pro\\Extensions\\php\\php7.3.4nts\\php.exe think syncWechatPayResult

[supervisord]
nodaemon=true
logfile = E:\dev\tp51\app\runtime\log\supervisord.log
pidfile = E:\dev\tp51\app\runtime\log\supervisord.pid

[supervisorctl]

启动

start.bat

::守护进程应设置任务计划,开机时启动
supervisord -c supervisord.conf

title: PHP-RabbitMQ-2高级特性
date: 2022-11-06
categories:

  • PHP
  • 中间件
    tags:
  • RabbitMQ
  • PHP

常见问题

  • 生产者消息送达失败
  • 重复消费
  • 消息没有成功消费
  • 消息N年后一直没有被销毁
  • 高并发
  • 消息丢失后无法找回

PhpAmqpLib

php-amqplib是Advanced Message Queuing Protocol (AMQP)的一个PHP开源实现。

高级消息队列协议(AMQP)是一个异步消息传递所使用的应用层协议规范。作为线路层协议,而不是API(例如JMS),AMQP 客户端能够无视消息的来源任意发送和接受信息。

安装

composer require php-amqplib/php-amqplib

或直接引用

"autoload": {
        "psr-4": {
            "PhpAmqpLib\\": "PhpAmqpLib/"
        }
    },

使用

use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

示例下载:php-amqplib-master

链接: https://pan.baidu.com/s/1tk26bbQyL8frNPZRf8EJug 提取码: d5ad

高级特性

  • ACK(confirm机制)
  • 如何保证消息百分百投递成功
  • 幂等性
  • return机制
  • 限流
  • 重回队列
  • TTL
  • 死信队列

1 ACK(confirm机制)

概念

消费者完成一个任务可能需要一段时间,如果其中一个消费者处理一个长的任务并仅只完成了部分突然它挂掉了,会导致消息丢失。RabbitMQ 一旦向消费者传递了一条消息,便立即将该消息标记为删除。在这种情况下,突然有个消费者挂掉了,我们将丢失正在处理的消息。以及后续发送给该消费这的消息,因为它无法接收到。

为了保证消息在发送过程中不丢失,rabbitmq 引入消息应答机制,消息应答就是:消费者在接收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了。

自动应答

消息发送后立即被认为已经传送成功

弊端:如果消息在接收到之前,消费者那边出现连接或者 信道 关闭,那么消息就丢 失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制, 当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终 使得内存耗尽,最终这些消费者线程被操作系统杀死

所以在实际开发中我们应选择手动应答

消息应答的方法

Channel.basicAck():用于肯定确认

RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了

Channel.basicNack():用于否定确认

Channel.basicReject():用于否定确认 (推荐使用

实现

  1. 在channel上开启确认模式:$channel->confirm_select();
  2. 在channel上添加监听:$channel->wait_for_pending_acks();监听成功和失败的返回结果,根据具体的结果对消息进行重新发送、或记录日志等后续处理。

代码

生产者
<?php
require_once __DIR__.'/vendor/autoload.php';  

use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;

//申明连接参数
$config = [
    'host' => '192.168.31.51',
    'vhost' => '/',
    'port' => 5672,
    'login' => 'test',
    'password' => '123456'
];
$exchange = 'ack_exchange';
$connection = new AMQPStreamConnection($config['host'], $config['port'], $config['login'], $config['password'], $config['vhost']);
$channel = $connection->channel();
//推送成功
$channel->set_ack_handler(
    function (AMQPMessage $message) {
        echo "set_ack_handler:" . $message->body . PHP_EOL;
    }
);
//推送失败
$channel->set_nack_handler(
    function (AMQPMessage $message) {
        echo "set_nack_handler:" . $message->body . PHP_EOL;
    }
);
$channel->confirm_select();
$channel->exchange_declare($exchange, 'fanout', false, false, true);
$msg = new AMQPMessage('xxx', array('content_type' => 'text/plain'));
$channel->basic_publish($msg, $exchange);
$channel->wait_for_pending_acks();
$channel->close();
$connection->close();
消费者
<?php
require_once __DIR__.'/vendor/autoload.php';

use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Exchange\AMQPExchangeType;

$config = [
    'host'=>'192.168.31.51',
    'vhost'=>'/',
    'port'=>5672,
    'login'=>'test',
    'password'=>'123456'
];

$exchange = 'ack_exchange';
$queue = 'ack_queue';

$connection = new AMQPStreamConnection($config['host'], $config['port'], $config['login'], $config['password'], $config['vhost']);
$channel = $connection->channel();

$channel->queue_declare($queue, false, true, false, false);
$channel->exchange_declare($exchange, AMQP_EX_TYPE_FANOUT, false, false, true);
$channel->queue_bind($queue, $exchange);


function process_message($message)
{
    echo "成功收到消息,消息内容为:".$message->body ;
    //消费完消息之后进行应答,告诉rabbit我已经消费了,可以发送下一组了
    $message->delivery_info['channel']->basic_ack($message->delivery_info['delivery_tag']);
}

$channel->basic_consume($queue, '', false, false, false, false, 'process_message');
while ($channel->is_consuming()) {
    // After 10 seconds there will be a timeout exception.
    $channel->wait(null, false, 30000);
}

2 确保百分百投递成功

方案一 消息落库打标(常用)

将消息持久化到DB并设置状态值,收到Consumer的应答就改变当前记录的状态。
再轮询重新发送没接收到应答的消息,注意这里要设置重试次数。

实现流程

下单成功:

  1. 对订单数据入BIZ DB订单库,并对因此生成的业务消息入MSG DB消息库(状态0)

    注:一定要保证消息都存储成功了,生产端再进行消息发送。如果失败了就进行快速失败机制

  2. 发送消息到 MQ 服务上
  3. 生产端有一个 Confirm Listener,异步监听Broker回送的响应,从而判断消息是否投递成功

    1. 如果成功,去数据库查询该消息,并将消息状态更新为1
    2. 如果出现意外情况,消费者未接收到或者 Listener 接收确认时发生网络闪断,导致生产端的Listener就永远收不到这条消息的confirm应答了,也就是说这条消息的状态就一直为0了,这时候就需要用到我们的分布式定时任务来从 MSG 数据库抓取那些超时了还未被消费的消息,重新发送一遍。
    3. 此时我们需要设置一个规则,比如说消息在入库时候设置一个临界值timeout,5分钟之后如果还是0的状态那就需要把消息抽取出来。这里我们使用的是分布式定时任务,去定时抓取DB中距离消息创建时间超过5分钟的且状态为0的消息。
  4. 把抓取出来的消息进行重新投递(Retry Send),也就是从第二步开始继续往下走
  5. 有些消息可能就是由于一些实际的问题无法路由到Broker,比如routingKey设置不对,对应的队列被误删除了,那么这种消息即使重试多次也仍然无法投递成功,所以需要对重试次数做限制,比如限制3次, 如果投递次数大于三次,那么就将消息状态更新为2,表示这个消息最终投递失败,然后通过补偿机制,人工去处理。实际生产中,这种情况还是比较少的,但是你不能没有这个补偿机制,要不然就做不到可靠性了。

缺点: 在第一步需要更新或者插入操作数据库2次;在大厂中 都不会加事务,都是进行的补偿操作。

优化:不需要消息进行持久化 只需要业务持久化

方案二 消息二次确认

消息的延迟投递,做二次确认,回调检查(不常用,大厂在用的高并发方案)

  1. (上游服务:Upstream service)业务入库 然后send 消息到broker
  2. 进行消息延迟发送到新的queue(延迟时间为5分钟:业务决定)
  3. (下游服务:Downstream service)监听到消息然后处理消息
  4. 下游服务send confirm生成新的消息到broker (这里是一个新的queue)
  5. callback service 去监听这个消息 并且入库 如果监听到表示这个消息已经消费成功
  6. callback service 去检查 第二步投递的延迟消息是否 在msgDB里面是否消费成功,不存在或者消费失败就会 Resend command
  7. 如果在第1,2,4步失败 ,如果成功 broker会给我们一个confirm,失败当然没有,这是消息可靠性投递的重要保障

3 幂等性

定义:用户对于同一操作发起的一次请求或者多次请求的结果是一致的。

实现方案:唯一ID+指纹码,利用数据库主键去重

唯一ID: 业务表的主键 指纹码:为了区别每次正常操作的码,每次操作时生成指纹码;可以用时间戳+业务编号或者标志位(具体视业务场景而定)

缺点:高并发下有数据库写入的性能瓶颈

解决方案:根据ID进行分库分表算法路由

4 return机制

Return Listener用于处理未找到交换机或未路由到队列的消息。也是生产段添加的一个监听。

示例:打印错误信息

<?php

require_once __DIR__ . '/../vendor/autoload.php';

use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Exchange\AMQPExchangeType;
use PhpAmqpLib\Message\AMQPMessage;

$connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
$channel = $connection->channel();

// declare  exchange but don`t bind any queue
$channel->exchange_declare('hidden_exchange', AMQPExchangeType::TOPIC);

$message = new AMQPMessage("Hello World!");

$wait = true;

$returnListener = function(
    $replyCode,
    $replyText,
    $exchange,
    $routingKey,
    $message
) use ($wait){
    $GLOBALS['wait'] = false;
    echo "return:",
    $replyCode,"\n",
    $replyText,"\n",
    $exchange,"\n",
    $routingKey,"\n",
    $message->body,"\n";
};

$channel->set_return_listener($returnListener);

//echo " [x] Sent non-mandatory ...";
$channel->basic_publish(
    $message,
    'hidden_exchange',
    'rkey',
    ture
);
//echo " done.\n";


while ($wait) {
    $channel->wait();
}

$channel->close();
$connection->close();

5 限流机制

RabbitMQ提供了一种qos (服务质量保证)功能,即在非自动确认消息的前提下,如果一定数目的消息 (通过基于Con或者channel设置Qos的值) 未被确认前,不消费新的消息

限流设置
$channel->basic_qos($prefetchSize, 20, $global);
  • prefetchSize: 单条消息的大小限制,Con通常设置为0,表示不做限制
  • prefetchCount: 一次最多能处理多少条消息
  • global: 是否将上面设置true应用于channel级别还是取false代表Con级别

注:prefetchCount在 autoAck=false 的情况下生效,即在自动应答的情况下该值无效

手工ACK

prefetchCount在 autoAck=false 的情况下生效,即在自动应答的情况下该值无效

$message->delivery_info['channel']->basic_ack($message->delivery_info['delivery_tag']);

调用这个方法就会主动回送给Broker一个应答,表示这条消息我处理完了,你可以给我下一条了。

参数multiple表示是否批量签收,由于我们是一次处理一条消息,所以设置为false

如果注释掉这行,会反复消费

实现
<?php

include(__DIR__ . '/config.php');

use PhpAmqpLib\Connection\AMQPStreamConnection;

$connection = new AMQPStreamConnection(HOST, PORT, USER, PASS, VHOST);

$channel = $connection->channel();

$channel->queue_declare('qos_queue', false, true, false, false);

//第二个参数代表:每次只消费100条
$channel->basic_qos(null, 109, null);

function process_message($message)
{
    /*业务逻辑*/
    //消费完消息之后进行应答,告诉rabbit我已经消费了,可以发送下一组了
    $message->delivery_info['channel']->basic_ack($message->delivery_info['delivery_tag']);
}

$channel->basic_consume('qos_queue', '', false, false, false, false, 'process_message');

while ($channel->is_consuming()) {
    // After 10 seconds there will be a timeout exception.
    $channel->wait(null, false, 30000);
}

注:$message->ack();就是封装过的$channel->basic_ack();

6 重回队列

当我们设置 autoACK=false时,就可以使用手工ACK方式了,其实手工方式包括了手工ACK与NACK。

  • 手工 ACK 时,会发送给Broker一个应答,代表消息处理成功,Broker就可回送响应给Pro;
  • NACK 表示消息处理失败,如果设置了重回队列,Broker端就会将没有成功处理的消息重新发送。
重回队列
  • 重回队列是为了对没有处理成功的消息,将消息重新投递给Broker
  • 重回队列,会把消费失败的消息重新添加到队列的尾端,供Con继续消费
  • 一般在实际应用中,都会关闭重回队列,即设置为false
具体实现
<?php
/**
 * - Start this consumer in one window by calling: php demo/basic_nack.php
 * - Then on a separate window publish a message like this: php demo/amqp_publisher.php good
 *   that message should be "ack'ed"
 * - Then publish a message like this: php demo/amqp_publisher.php bad
 *   that message should be "nack'ed"
 */
include(__DIR__ . '/config.php');

use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Exchange\AMQPExchangeType;

$exchange = 'router';
$queue = 'msgs';
$consumerTag = 'consumer';

$connection = new AMQPStreamConnection(HOST, PORT, USER, PASS, VHOST);
$channel = $connection->channel();

$channel->queue_declare($queue, false, true, false, false);
$channel->exchange_declare($exchange, AMQPExchangeType::DIRECT, false, true, false);
$channel->queue_bind($queue, $exchange);

/**
 * @param \PhpAmqpLib\Message\AMQPMessage $message
 */
function process_message($message)
{
    /*
    if(插入成功){
        echo "将消息删除:";
        服务器死掉,相当于exit;
        $message->ack(true);
    }else if(插入失败){
        echo "将消息不要删除,等着下次消费";
        $message->nack(true);
    }*/

    if ($message->body == 'good') {
        $message->ack();
    } else {
        echo "成功收到消息,消息内容为:".$message->body ;
        echo "将消息打回,重回队列:";
        $message->nack(true);
    }

    // Send a message with the string "quit" to cancel the consumer.
    if ($message->body === 'quit') {
        $message->getChannel()->basic_cancel($message->getConsumerTag());
    }
}

$channel->basic_consume($queue, $consumerTag, false, false, false, false, 'process_message');

/**
 * @param \PhpAmqpLib\Channel\AMQPChannel $channel
 * @param \PhpAmqpLib\Connection\AbstractConnection $connection
 */
function shutdown($channel, $connection)
{
    $channel->close();
    $connection->close();
}

register_shutdown_function('shutdown', $channel, $connection);

// Loop as long as the channel has callbacks registered
while ($channel->is_consuming()) {
    $channel->wait();
}

7 TTL

  • TTL(Time To Live),即生存时间
  • RabbitMQ支持消息的过期时间,在消息发送时可以进行指定
  • RabbitMQ支持为每个队列设置消息的超时时间,从消息入队列开始计算,只要超过了队列的超时时间配置,那么消息会被自动清除
实现
// 消息过期方式:设置 queue.normal 队列中的消息10s之后过期 
$args->set('x-message-ttl', 10000);
$args->set('x-dead-letter-exchange', 'exchange.dlx');
$args->set('x-dead-letter-routing-key', 'routingkey');

8 死信队列

DLX - 死信队列(dead-letter-exchange) 利用DLX,当消息在一个队列中变成死信 (dead message) 之后,它能被重新publish到另一个Exchange中,这个Exchange就是DLX.

产生场景

消息被拒绝(basic.reject / basic.nack),并且requeue = false 消息因TTL过期 队列达到最大长度

处理过程

DLX亦为一个普通的Exchange,它能在任何队列上被指定,实际上就是设置某个队列的属性 当某队列中有死信时,RabbitMQ会自动地将该消息重新发布到设置的Exchange,进而被路由到另一个队列 可以监听这个队列中的消息做相应的处理.该特性可以弥补RabbitMQ 3.0以前支持的immediate参数的功能

配置

设置死信队列的exchange和queue,然后进行绑定 - Exchange:dlx.exchange - Queue: dlx.queue - RoutingKey:# 正常声明交换机、队列、绑定,只不过我们需要在队列加上一个参数即可arguments.put(" x-dead-letter- exchange","dlx.exchange");

这样消息在过期、requeue、 队列在达到最大长度时,消息就可以直接路由到死信队列!

实现
<?php
include(__DIR__ . '/config.php');
use PhpAmqpLib\Wire\AMQPTable;
use PhpAmqpLib\Message\AMQPMessage;
use PhpAmqpLib\Exchange\AMQPExchangeType;
use PhpAmqpLib\Connection\AMQPStreamConnection;

/**
 * 死信队列测试
 * 1、创建两个交换器 exchange.normal 和 exchange.dlx, 分别绑定两个队列 queue.normal 和 queue.dlx
 * 2、把 queue.normal 队列里面的消息配置过期时间,然后通过 x-dead-letter-exchange 指定死信交换器为 exchange.dlx
 * 3、发送消息到 queue.normal 中,消息过期之后流入 exchange.dlx,然后路由到 queue.dlx 队列中,进行消费
 */

// todo 更改配置
//$connection = new AMQPStreamConnection('192.168.33.1', 5672, 'zhangcs', 'zhangcs', '/');
$connection = new AMQPStreamConnection(HOST, PORT, USER, PASS, VHOST);

$channel = $connection->channel();

$channel->exchange_declare('exchange.dlx', AMQPExchangeType::DIRECT, false, true);
$channel->exchange_declare('exchange.normal', AMQPExchangeType::FANOUT, false, true);
$args = new AMQPTable();
// 消息过期方式:设置 queue.normal 队列中的消息10s之后过期
$args->set('x-message-ttl', 10000);
// 设置队列最大长度方式: x-max-length
//$args->set('x-max-length', 1);
$args->set('x-dead-letter-exchange', 'exchange.dlx');
$args->set('x-dead-letter-routing-key', 'routingkey');
$channel->queue_declare('queue.normal', false, true, false, false, false, $args);
$channel->queue_declare('queue.dlx', false, true, false, false);

$channel->queue_bind('queue.normal', 'exchange.normal');
$channel->queue_bind('queue.dlx', 'exchange.dlx', 'routingkey');
$message = new AMQPMessage('Hello DLX Message');
$channel->basic_publish($message, 'exchange.normal', 'rk');

$channel->close();
$connection->close();

常见问题

1.Docker环境安装扩展时报错git was not found in your PATH, skipping source download

解决:https://blog.csdn.net/phpstory/article/details/116016980

apt --fix-broken install
apt-get update
apt-get upgrade
 
apt-get install zip

2.Call to undefined function PhpAmqpLib\Wire\bcmod

原因:PHP缺少bcmath扩展

docker-php-ext-install -j$(nproc) bcmath

参考

PHP进阶 RabbitMQ深入浅出

RabbitMQ 中文文档

Windows安装RabbitMQ详细教程](https://zhuanlan.zhihu.com/p/534570980)

基于 Docker 安装 RabbitMQ

docker安装PHP扩展

PHP 使用rabbitmq 入门教程

php使用rabbitMQ

基于RabbitMQ实现延迟队列--PHP版

教你如何在 Windows 下让崩溃的 Python 程序自重启

基于 Docker 安装 RabbitMQ


IT小马
1.2k 声望166 粉丝

Php - Go - Vue - 云原生


引用和评论

0 条评论