基于laravel探索rabbitmq的高级特性
基于源码实现MQ的生产与消费
1 . 封装连接方法 connection.php
<?php
$config = array(
'host' => '172.17.0.5',
'vhost'=> 'my_vhost', //设置虚拟机,相当于mysql的库
'port' => 5672,
'login'=> 'admin',
'password' => 'admin'
);
try {
//创建AMQP对象
$cnn = new AMQPConnection($config);
//连接MQbroker
if (!$cnn->connect()) {
echo 'Cannot connect to the broker';
exit();
}
//在连接内创建一个通道
$ch = new AMQPChannel($cnn);
//创建一个交换机
$ex = new AMQPExchange($ch);
//设置交换机名称
$ex->setName($exchangeName);
/**
* 设置交换机类型
* AMQP_EX_TYPE_DIRECT:直连交换机
* AMQP_EX_TYPE_FANOUT:扇形交换机
* AMQP_EX_TYPE_HEADERS:头交换机
* AMQP_EX_TYPE_TOPIC:主题交换机
*/
$ex->setType(AMQP_EX_TYPE_DIRECT);
//设置交换机持久化
$ex->setFlags(AMQP_DURABLE);
//声明交换机
$ex->declareExchange();
} catch (AMQPConnectionException $e) {
echo '创建连接异常:'.$e->getMessage();
exit();
} catch (AMQPExchangeException $e) {
echo '创建交换机异常:'.$e->getMessage();
exit();
} catch (AMQPChannelException $e) {
echo '创建通道异常:'.$e->getMessage();
exit();
}
2 . 新建consumer.php 消费者
<?php
//声明一个路由键
$routingKey = 'key_1';
//设置一个交换机名称
$exchangeName = 'exchange_1';
//声明交换机
try {
include 'connection.php';
//创建一个消息队列
$q = new AMQPQueue($ch);
//设置队列名称
$q->setName('queue_1');
//设置队列持久化
$q->setFlags(AMQP_DURABLE);
//声明消息队列
$q->declareQueue();
//交换机和队列通过$routingKey进行绑定
$q->bind($ex->getName(),$routingKey);
//接受消息并进行处理的回调方法
function recevie($envelope,$queue){
sleep(0.01);
//显示消息
echo $envelope->getBody().PHP_EOL;
//处理业务逻辑;
//ack确认,队列收到消费者确认后,会删除消息
$queue->ack($envelope->getDeliveryTag());
}
//设置消息队列消费者回调方法
$q->consume('recevie');
} catch (AMQPConnectionException $e) {
echo '创建连接异常:'.$e->getMessage();
exit();
} catch (AMQPChannelException $e) {
echo '创建通道异常:'.$e->getMessage();
exit();
} catch (AMQPQueueException $e) {
echo '创建消息队列异常:'.$e->getMessage();
exit();
} catch (AMQPEnvelopeException $e) {
echo '消息消费异常:'.$e->getMessage();
exit();
}
3 . 新建producer.php 生产者
<?php
//声明一个路由键
$routingKey = 'key_1';
//设置一个交换机名称
$exchangeName = 'exchange_1';
include 'connection.php';
//创建是个消息
for($i = 1;$i <= 100000; $i++){
$msg = array(
'data' => 'message_'.$i,
'hello'=> 'world'
);
//发送消息到交换机,并返回f发送结果
//delivery_mode:2声明消息持久,持久的队列 + 持久的消息在rabbitmq重启后不会消失
//代码执行完毕后进程会自动退出
try {
echo "Send Message:" . $ex->publish(json_encode($msg), $routingKey
, AMQP_NOPARAM, array('delivery_mode' => 2)) . "\n";
} catch (AMQPChannelException $e) {
echo '创建消息通道异常:'.$e->getMessage();
exit();
} catch (AMQPConnectionException $e) {
echo '创建连接异常:'.$e->getMessage();
exit();
} catch (AMQPExchangeException $e) {
echo '创建交换机异常:'.$e->getMessage();
exit();
}
}
rabbitMQ的八大特性
当前案例,是直接使用 laravel 中 对rabbitMQ的扩展包进行实现操作
案例源码:
git clone git@gitee.com:dear-q/rabbitmq-demo.git
参考文献:
RabbitMQ各方法详解 :
https://blog.csdn.net/qq_34272964/java/article/details/103937896
中文文档:
https://rabbitmq.shujuwajue.com/ying-yong-jiao-cheng/php-ban/2-work_queues.md
八大特性:
- ACK(confirm机制)
- 如何保证消息百分百投递成功
- 幂等性
- return机制
- 限流
- 重回队列
- TTL
- 死信队列
能解决的问题
- 生产者消息送达失败
- 重复消费
- 消息没有成功消费
- 消息N年后一直没有被销毁
- 高并发
- 消息丢失后无法找回
目录结构:
1 . ACK(confirm机制)
1.1 什么是Confirm机制
Pro发送消息到MQ,MQ接收到消息后,产生回响应给Pro,Pro中有一个Confirm Listener异步监听响应应答
步骤:
- 消息的确认 Pro投递消息后,如果MQ收到消息,则会给Pro一个应答
- Pro接收应答 用来确定这条消息是否正常地发送到MQ,该法也是消息可靠性投递的核心保障!
1.2 Confirm机制流程图
1.3 实现Confirm机制
- 在channel上开启确认模式:$channel->confirm_select();
- 在channel上添加监听:$channel->wait_for_pending_acks();监听成功和失败的返回结果,根据具体的结果对消息进行重新发送、或记录日志等后续处理。
<?php
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
include(__DIR__.'/config.php');
$exchange_name = 'someExchange';
$queue_name = 'someQueue';
//创建连接实力
$connection = new AMQPStreamConnection(HOST,PORT,USER,PASS,VHOST);
//创建通道
$channel = $connection->channel();
// 开启confirm模式,confirm模式下,投递消息后,RabbitMQ会异步返回是否投递成功,confirm模式不可以和事务模式同时存在
$channel->confirm_select();
// 推送消息到RabbitMQ成功的异步回调,如果消息推送成功,想做什么业务处理写在这里
$channel->set_ack_handler(function (AMQPMessage $message){
echo "Message acked with content " . $message->body . PHP_EOL;
});
// 推送消息到RabbitMQ失败的异步回调,如果消息推送失败,想做什么业务处理写在这里
$channel->set_nack_handler(function (AMQPMessage $message){
echo "Message nacked with content " . $message->body . PHP_EOL;
});
//声明交换机,将第四个参数设置为true,表示将交换机持久化
$channel->exchange_declare($exchange_name,AMQP_EX_TYPE_FANOUT,false,false,true);
//声明队列名称,将第三个参数设置为true,表示将队列持久化
$channel->queue_declare($queue_name,false, true, false, false);
//队列与交换机绑定,因为是扇形交换机,直接与队列绑定,无需与路由键绑定
$channel->queue_bind($queue_name,$exchange_name);
$i = 1;
while ($i <= 100000){
//消息必须是string或者int类型
$content = '这是消息内容' . $i++;
//消息类,设置delivery_mode为DELIVERY_MODE_PERSISTENT,表示将消息持久化
$msg = new AMQPMessage($content,[
'content_type' => 'text/plain',
'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT
]);
//消息推送
$channel->basic_publish($msg,$exchange_name);
}
//监听写入
$channel->wait_for_pending_acks();
$channel->close();
$connection->close();
2 . 保证消息的百分百投递成功
2.1 Producer 的可靠性投递
2.1.1 要求
- 保证消息的成功发出
- 保证MQ节点的成功接收
- 发送端收到MQ节点(Broker) 确认应答
- 完善的消息补偿机制
在实际生产中,很难保障前三点的完全可靠,比如在极端的环境中,生产者发送消息失败了,发送端在接受确认应答时突然发生网络闪断等等情况,很难保障可靠性投递,所以就需要有第四点完善的消息补偿机制。
2.1.2 解决方案
2.1.2.1 方案一:消息信息落库,对消息状态进行打标(常见方案)
将消息持久化到DB并设置状态值,收到Consumer的应答就改变当前记录的状态.
再轮询重新发送没接收到应答的消息,注意这里要设置重试次数.
2 . 2 . 3 实现
// 开启confirm模式,confirm模式下,投递消息后,RabbitMQ会异步返回是否投递成功,confirm模式不可以和事务模式同时存在
$channel->confirm_select();
// 推送消息到RabbitMQ成功的异步回调,如果消息推送成功,想做什么业务处理写在这里
$channel->set_ack_handler(function (AMQPMessage $message) {
//update 订单表 set is_send_succ=ture
});
// 推送消息到RabbitMQ失败的异步回调,如果消息推送失败,想做什么业务处理写在这里
$channel->set_nack_handler(function (AMQPMessage $message) {
//update 订单表 set is_send_succ=false
});
3 . 幂等性
3.1 什么是幂等性
用户对于同一操作发起的一次请求或者多次请求的结果是一致的
在业务高峰期最容易产生消息重复消费问题,当Con消费完消息时,在给Pro返回ack时由于网络中断,导致Pro未收到确认信息,该条消息就会重新发送并被Con消费,但实际上该消费者已成功消费了该条消息,这就造成了重复消费.
而Con - 幂等性,即消息不会被多次消费,即使我们收到了很多一样的消息.
3.2 主流幂等性实现方案
为了避免之后消费者消费消息时可能产生的重复消费问题,我们最好在消息中添加一个唯一ID(自己设计生成),这样之后消费者消费消息时先去缓存中查有没有消费过这个消息。
如果有消费过,则不再处理并且直接ack让rabbitmq删除这条消息。
如果缓存中没有这个ID,则说明没有消费过这条消息,那就先消费执行业务逻辑,执行成功后将这个ID写入缓存,然后ack确认让rabbitmq删除掉这条消息。
这里需要注意的是,建议消费者将唯一ID存到缓存中时,设置个有效期TTL,这样可以避免内存爆炸。一般设置为1-2天足以了,因为即使有失败的消息,我们的业务人员也会在1-2天内手动处理好。
4 . Return机制
4.1 什么是Return机制
— Return Listener用于处理一些不可路由的消息。也是生产段添加的一个监听。
我们的消息生产者,通过指定一个Exchange和Routingkey,把消息送达到某一个队列中去,然后我们的消费者监听队列,进行消息处理操作。但是在某些情况下,如果我们在发送消息的时候,当前的exchange不存在 或者指定的路由key路由不到,这个时候如果我们需要监听这种不可达的消息,就要使用Return Listener。
4.2 图解Return机制
4.3 实现Return机制
<?php
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
include_once __DIR__.'/config.php';
$exchange_name = 'test1_exchange';
$routing_key = 'test1_routing_key';
//创建连接
$connection = new AMQPStreamConnection(HOST,PORT,USER,PASS,VHOST);
$channel = $connection->channel();
//测试不绑定路由器不绑定交换机,直接发送消息
$channel->exchange_declare($exchange_name,AMQP_EX_TYPE_DIRECT);
$centent = '这是一个漂亮的demo';
$message = new AMQPMessage($centent);
//消息推送,第四个参数设置为true表示输出消息推送是的erro
$channel->basic_publish($message,$exchange_name,$routing_key,true);
$wait = true;
//监听消息模板
$returnListener = function (
$replyCode,
$replyText,
$exchange,
$routingKey,
$message
) use(&$wait){
$wait = false;
echo "return:".PHP_EOL,
"replyCode :$replyCode".PHP_EOL,
"replyText :$replyText".PHP_EOL,
"exchange :$exchange".PHP_EOL,
"routingKey :$routingKey".PHP_EOL,
"message :$message->body".PHP_EOL;
};
//设置监听模式
$channel->set_return_listener($returnListener);
while ($wait) {
//消息循环输出到控制台
$channel->wait();
}
$channel->close();
$connection->close();
?>
测试结果:
5 . 限流机制
5.1 Con - 限流机制
为何要限流:
假设我们有这样的场景 Rabbitmq服务器有上万条未处理的消息,我们随便打开一个Con - Client,会造成:巨量的消息瞬间全部推送过来,然而我们单个客户端无法同时处理这么多数据!此时很有可能导致服务器崩溃,严重的可能导致线上的故障。
还有一些其他的场景,比如说单个Pro一分钟产生了几百条数据,但是单个Con一分钟可能只能处理60条,这个时候Pro-Con肯定是不平衡的。通常Pro是没办法做限制的。所以Con肯定需要做一些限流措施,否则如果
超出最大负载,可能导致Con性能下降,服务器卡顿甚至崩溃等一系列严重后果
RabbitMQ提供了一种qos (服务质量保证)功能,即在非自动确认消息的前提下,如果一定数目的消息 (通过基于Con或者channel设置Qos的值) 未被确认前,不消费新的消息
不能设置自动签收功能(autoAck = false) 如果消息未被确认,就不会到达Con,目的就是给Pro减压
限流设置API
$channel->basic_qos($prefetchSize, 20, $global);
- prefetchSize: 单条消息的大小限制,Con通常设置为0,表示不做限制
- prefetchCount: 一次最多能处理多少条消息
- global: 是否将上面设置true应用于channel级别还是取false代表Con级别
prefetchSize和global这两项,RabbitMQ没有实现,暂且不研究 prefetchCount在 autoAck=false 的情况下生效,即在自动应答的情况下该值无效
手工ACK
$message->delivery_info['channel']->basic_ack($message->delivery_info['delivery_tag']);
调用这个方法就会主动回送给Broker一个应答,表示这条消息我处理完了,你可以给我下一条了。参数multiple表示是否批量签收,由于我们是一次处理一条消息,所以设置为false
5 . 2 实现
<?php
/**
* 限流机制
*/
use PhpAmqpLib\Connection\AMQPStreamConnection;
include_once __DIR__.'/config.php';
/*$exchange_name = 'test_exchange';
$routing_key = 'test_routing_key';
$queue_name = 'test_queue';*/
$exchange_name = 'someExchange';
$routing_key = 'someRoutking';
$queue_name = 'someQueue';
$connection = new AMQPStreamConnection(HOST,PORT,USER,PASS,VHOST);
$channel = $connection->channel();
$channel->queue_declare($queue_name,false, true, false, false);
$channel->queue_bind($queue_name,$exchange_name,$routing_key);
//限流机制开启,参数2000代表当前服务每次只能消费2000条数据,消费完成后继续从MQ中获取2000数据慢慢消费
$channel->basic_qos(null,2000,null);
/**
* 回调方法
* @param $message
*/
function process_message($message){
/*业务逻辑*/
echo "成功收到消息,消息内容为:{$message->body}".PHP_EOL;
//消费完消息之后进行应答,告诉rabbitmq我已经消费了消息
//如果把2000条消息都消费完了,则会在发2000条消息到当前服务器中慢慢消费
//设置$message->ack()也能消费,不过不能起到一个basic_qos限流的作用
//$message->ack();
$message->delivery_info['channel']->basic_ack($message->delivery_info['delivery_tag']);
}
//启动消费者,消费消息
$channel->basic_consume($queue_name,'',false, false, false, false, 'process_message');
//判断是否消费,依次循环打印数据
while ($channel->is_consuming()){
//消息循环输出到控制台,这里主要输出错误信息之类的
$channel->wait();
}
6 . Con - ACK & 重回队列机制
6.1 ACK & NACK
当我们设置 autoACK=false 时,就可以使用手工ACK方式了,其实手工方式包括了手工ACK与NACK 当我们手工 ACK 时,会发送给MQ一个应答,代表消息处理成功 , MQ就可回送响应给Pro. NACK 则表示消息处理失败,如果设置了重回队列,MQ就会将没有成功处理的消息重新发送.
使用方式
- Con消费时,如果由于业务异常,我们可以手工 NACK 记录日志,然后进行补偿API:void basicNack(long deliveryTag, boolean multiple, boolean requeue)API:void basicAck(long deliveryTag, boolean multiple)
- 如果由于服务器宕机等严重问题,我们就需要手工 ACK 保障Con消费成功
6.2 重回队列
- 重回队列是为了对没有处理成功的消息,将消息重新投递给Broker
- 重回队列,会把消费失败的消息重新添加到队列的尾端,供Con继续消费
- 一般在实际应用中,都会关闭重回队列,即设置为false
6.3 实现机制
生产者
<?php
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
$exchange_name = 'router';
$queue_name = 'msgs';
$consumerTag = 'consumer';
include_once __DIR__.'/config.php';
$connectsion = new AMQPStreamConnection(HOST,PORT,USER,PASS,VHOST);
$channel = $connectsion->channel();
//开启监听模式
$channel->confirm_select();
//监听数据,成功
$channel->set_ack_handler(function (AMQPMessage $message){
//update 订单表 set is_send_succ=ture
echo "Message acked with content " . $message->body . PHP_EOL;
});
//监听数据,失败
$channel->set_nack_handler(function (AMQPMessage $message){
//update 订单表 set is_send_succ=false
echo "Message nacked with content " . $message->body . PHP_EOL;
});
// 将第三个参数设置为true,表示将队列持久化
$channel->queue_declare($queue_name,false,true,false,false);
// 将第四个参数设置为true,表示将交换机持久化
$channel->exchange_declare($exchange_name,AMQP_EX_TYPE_DIRECT,false,true,false);
$channel->queue_bind($queue_name,$exchange_name);
$i = 1;
while ($i <= 100){
$content = '这是消息内容' . $i++;
//实力消息类
// 设置delivery_mode为DELIVERY_MODE_PERSISTENT,表示将消息持久化
$msg = new AMQPMessage($content,['content_type' => 'text/plain', 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]);
$channel->basic_publish($msg,$exchange_name);
}
//监听写入
$channel->wait_for_pending_acks();
$channel->close();
$connectsion->close();
消费者
<?php
use PhpAmqpLib\Connection\AMQPStreamConnection;
$exchange_name = 'router';
$queue_name = 'msgs';
$consumerTag = 'consumer';
include_once __DIR__.'/config.php';
$connectsion = new AMQPStreamConnection(HOST,PORT,USER,PASS,VHOST);
$channel = $connectsion->channel();
$channel->queue_declare($queue_name,false,true,false,false);
$channel->exchange_declare($exchange_name,AMQP_EX_TYPE_DIRECT,false,true,false);
$channel->queue_bind($queue_name,$exchange_name);
/**
* @param \PhpAmqpLib\Message\AMQPMessage $message
*/
function process_message($message){
if($message->body == 'good'){
echo "成功收到消息,消息内容为:{$message->body}";
$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_name,$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支持为每个队列设置消息的超时时间,从消息入队列开始计算,只要超过了队列的超时时间配置,那么消息会被自动清除
7.1 实现TTL
实现代码
// 消息过期方式:设置 queue.normal 队列中的消息10s之后过期
$args->set('x-message-ttl', 10000);
执行代码
<?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(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);
//给队列queue.normal绑定死信队列
//设置死信队列绑定的交换机
$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');
//开启监听模式
$channel->confirm_select();
//监听数据,成功
$channel->set_ack_handler(function (AMQPMessage $message){
//update 订单表 set is_send_succ=ture
echo "Message acked with content " . $message->body . PHP_EOL;
});
//监听数据,失败
$channel->set_nack_handler(function (AMQPMessage $message){
//update 订单表 set is_send_succ=false
echo "Message nacked with content " . $message->body . PHP_EOL;
});
$i = 1;
while ($i <= 100){
$content = '这是消息内容' . $i++;
//实力消息类
// 设置delivery_mode为DELIVERY_MODE_PERSISTENT,表示将消息持久化
$msg = new AMQPMessage($content,['content_type' => 'text/plain', 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]);
$exchange_name = 'exchange.normal';
$channel->basic_publish($msg,$exchange_name,'rk');
}
//监听写入
$channel->wait_for_pending_acks();
$channel->close();
$connection->close();
8 死信队列机制
8.1 什么是死信队列
DLX - 死信队列(dead-letter-exchange) 利用DLX,当消息在一个队列中变成死信 (dead message) 之后,它能被重新publish到另一个Exchange中,这个Exchange就是DLX.
8.2 死信队列的产生场景
消息被拒绝(basic.reject / basic.nack),并且requeue = false 消息因TTL过期 队列达到最大长度
8.3 死信的处理过程
DLX亦为一个普通的Exchange,它能在任何队列上被指定,实际上就是设置某个队列的属性 当某队列中有死信时,RabbitMQ会自动地将该消息重新发布到设置的Exchange,进而被路由到另一个队列 可以监听这个队列中的消
息做相应的处理.该特性可以弥补RabbitMQ 3.0以前支持的immediate参数的功能
8.4 死信队列的配置
设置死信队列的exchange和queue,然后进行绑定 - Exchange:dlx.exchange - Queue: dlx.queue - RoutingKey:# 正常声明交换机、队列、绑定,只不过我们需要在队列加上一个参数即可
arguments.put(" x-dead-letter- exchange",“dlx.exchange”);
这样消息在过期、requeue、 队列在达到最大长度时,消息就可以直接路由到死信队列!
8 . 5 执行代码
这里的代码跟ttl中的代码一致
<?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(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);
//给队列queue.normal绑定死信队列
//设置死信队列绑定的交换机
$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');
//开启监听模式
$channel->confirm_select();
//监听数据,成功
$channel->set_ack_handler(function (AMQPMessage $message){
//update 订单表 set is_send_succ=ture
echo "Message acked with content " . $message->body . PHP_EOL;
});
//监听数据,失败
$channel->set_nack_handler(function (AMQPMessage $message){
//update 订单表 set is_send_succ=false
echo "Message nacked with content " . $message->body . PHP_EOL;
});
$i = 1;
while ($i <= 100){
$content = '这是消息内容' . $i++;
//实力消息类
// 设置delivery_mode为DELIVERY_MODE_PERSISTENT,表示将消息持久化
$msg = new AMQPMessage($content,['content_type' => 'text/plain', 'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]);
$exchange_name = 'exchange.normal';
$channel->basic_publish($msg,$exchange_name,'rk');
}
//监听写入
$channel->wait_for_pending_acks();
$channel->close();
$connection->close();
9 主题交换机:
9 . 1 . 主题交换机原理
发送到主题交换机(topic exchange)的消息不可以携带随意什么样子的路由键(routing_key),它的路由键必须是一个由.
分隔开的词语列表。这些单词随便是什么都可以,但是最好是跟携带它们的消息有关系的词汇。
以下是几个推荐的例子:“stock.usd.nyse”,“nyse.vmw”, “quick.orange.rabbit”。词语的个数可以随意,但是不要超过255字节。
绑定键也必须拥有同样的格式。主题交换机背后的逻辑跟直连交换机很相似 —— 一个携带着特定路由键的消息会被主题交换机投递给绑定键与之想匹配的队列。但是它的绑定键和路由键有两个特殊应用方式:
*
(星号) 用来表示一个单词.#
(井号) 用来表示任意数量(零个或多个)单词。
下边用图说明:
这个例子里,我们发送的所有消息都是用来描述小动物的。发送的消息所携带的路由键是由三个单词所组成的,这三个单词被两个.
分割开。路由键里的第一个单词描述的是动物的速度,第二个单词是动物的颜色,第三个是动物的种类。
所以它看起来是这样的: <celerity>.<colour>.<species>
。
我们创建了三个绑定:
Q1的绑定键为 *.orange.*
,
Q2的绑定键为 *.*.rabbit
和 lazy.#
。
这三个绑定键被可以总结为:
- Q1 对所有的桔黄色动物都感兴趣。
- Q2 则是对所有的兔子和所有懒惰的动物感兴趣。
一个携带有 quick.orange.rabbit
的消息将会被分别投递给这两个队列。
携带着 lazy.orange.elephant
的消息同样也会给两个队列都投递过去。
另一方面携带有 quick.orange.fox
的消息会投递给第一个队列,
携带有 lazy.brown.fox
的消息会投递给第二个队列。
携带有 lazy.pink.rabbit
的消息只会被投递给第二个队列一次,即使它同时匹配第二个队列的两个绑定。
携带着 quick.brown.fox
的消息不会投递给任何一个队列。
如果我们违反约定,发送了一个携带有一个单词或者四个单词("orange"
or "quick.orange.male.rabbit"
)的消息时,发送的消息不会投递给任何一个队列,而且会丢失掉。
但是另一方面,即使 "lazy.orange.male.rabbit"
有四个单词,他还是会匹配最后一个绑定,并且被投递到第二个队列中。
9 . 2 . 操作实例 :
9 . 2 . 1 匹配案例:
9 . 2 . 2 消费者 :receive_logs_topic.php
首先创建消费者,并设定队列与路由匹配的主题规则
<?php
include(__DIR__ . '/config.php');
use PhpAmqpLib\Connection\AMQPStreamConnection;
$exchange_name = 'topic_logs';
$queue_name = 'topic_logs';
$connection = new AMQPStreamConnection(HOST,PORT,USER,PASS,VHOST);
$channel = $connection->channel();
$channel->exchange_declare($exchange_name, 'topic', false, false, false);
$channel->queue_declare($queue_name,false,true,false,false);
$binding_keys = array_slice($argv, 1);
if (empty($binding_keys)) {
echo "Usage: $argv[0] [binding_key]\n";
exit(1);
}
foreach ($binding_keys as $binding_key) {
$channel->queue_bind($queue_name, $exchange_name, $binding_key);
}
echo ' [*] Waiting for logs. To exit press CTRL+C', "\n";
$callback = function ($msg) {
echo ' [x] ', $msg->delivery_info['routing_key'], ':', $msg->body, "\n";
};
$channel->basic_consume($queue_name, '', false, true, false, false, $callback);
while (count($channel->callbacks)) {
$channel->wait();
}
$channel->close();
$connection->close();
绑定如下 :
rabbitmq中 :
9 . 2 . 3 生产者:emit_log_topic.php
生产者根据路由键,依次吧消息推送到队列中
<?php
include(__DIR__ . '/config.php');
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
$exchange_name = 'topic_logs';
$queue_name = 'topic_logs';
$connection = new AMQPStreamConnection(HOST,PORT,USER,PASS,VHOST);
$channel = $connection->channel();
$channel->exchange_declare($exchange_name, 'topic', false, false, false);
// 将第三个参数设置为true,表示将队列持久化
$channel->queue_declare($queue_name,false,true,false,false);
$routing_key = isset($argv[1]) && !empty($argv[1]) ? $argv[1] : 'anonymous.info';
$data = implode(' ', array_slice($argv, 2));
if (empty($data)) $data = "Hello World!";
$msg = new AMQPMessage($data);
$channel->basic_publish($msg, 'topic_logs', $routing_key);
echo " [x] Sent ", $routing_key, ':', $data, " \n";
$channel->close();
$connection->close();
测试如下: