RabbitMQ基础概念
AMQP
即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件。消息中间件主要用于组件之间的解耦和通讯。
AMQP的主要特征是面向消息,队列,路由(包括点对点和发布/订阅),可靠性,安全。
RabbitMQ是一个开源的AMQP实现,服务器端使用Erlang语言编写。用于在分布式系统中存储赚发消息,具有很高的易用性和可使用性。
ConnectionFactory
ConnectionFactory为Connection的制造工厂
Connection
Connection是RabbitMQ的socket连接,他封装了socket协议相关部分逻辑
Channel(信道)
信道是建立在“真实的”TCP连接上的虚拟连接,在一条TCP连接上创建多少条信道是没有限制的,把他想象成光纤就可以了。它是我们与RabbitMQ打交道的最重要的一个接口,我们大部分的业务操作是在Channel这个接口中完成的,包括定义Queue,定义Exchange,绑定Queue和Exchange,发布消息等。
Queue
Queue是RabbitMQ的内部对象,用于存储消息。
RabbitMQ的消息只能存储在Queue中。生产者可以从Queue中获取消息并消费,消费者可以是一个或者多个。
Message Acknowledgment(ack消息的确认)
在实际应用中,可能会发生消费者收到Queue中的消息,但没有处理完成就宕机(或出现其他意外)的情况,这种情况下就可能会导致消息丢失。为了避免这种情况发生,我们可以要求消费者在消费完消息后发送一个回执给RabbitMQ,RabbitMQ收到消息回执(ack)后才将该消息从Queue中移除;如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则RabbitMQ会将该消息发送给其他消费者(如果存在多个消费者)进行处理。这里不存在timeout概念,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非它的RabbitMQ连接断开。
这里会产生另外一个问题,如果我们的开发人员在处理完业务逻辑后,忘记发送回执给RabbitMQ,这将会导致严重的bug——Queue中堆积的消息会越来越多;消费者重启后会重复消费这些消息并重复执行业务逻辑…
Message Durability(消息持久化)
如果我们希望即使在RabbitMQ服务重启的情况下,也不会丢失消息,我们可以将Queue与Message都设置为可持久化的(durable),这样可以保证绝大部分情况下我们的RabbitMQ消息不会丢失。但依然解决不了小概率丢失事件的发生(比如RabbitMQ服务器已经接收到生产者的消息,但还没来得及持久化该消息时RabbitMQ服务器就断电了),如果我们需要对这种小概率事件也要管理起来,那么我们要用到事务。
Prefetch count(每次向消费者发送消息的总数)
前面我们讲到如果有多个消费者同时订阅同一个Queue中的消息,Queue中的消息会被平摊给多个消费者。这时如果每个消息的处理时间不同,就有可能会导致某些消费者一直在忙,而另外一些消费者很快就处理完手头工作并一直空闲的情况。我们可以通过设置prefetchCount来限制Queue每次发送给每个消费者的消息数,比如我们设置prefetchCount=1,则Queue每次给每个消费者发送一条消息;消费者处理完这条消息后Queue会再给该消费者发送一条消息。
Exchange(交换器)
Exchange生产者将消息发送到 Exchange(交换器,下图中的X),由 Exchange 根据一定的规则将消息路由到一个或多个 Queue 中(或者丢弃)。
Routing key(路由key)
生产者在将消息发送给 Exchange 的时候,一般会指定一个 routing key,来指定这个消息的路由规则。 Exchange 会根据 routing key 和 Exchange Type(交换器类型) 以及 Binding key 的匹配情况来决定把消息路由到哪个 Queue。RabbitMQ为routing key设定的长度限制为255 bytes。
Binding(绑定)
RabbitMQ中通过Binding将Exchange与Queue关联起来
Binding Key
在绑定(Binding)Exchange与Queue关系的同时,一般会指定一个binding key
Exchange Types(交换器类型)
RabbitMQ常用的Exchange Type有Fanout、Direct、Topic、Headers这四种
Fanout
这种类型的Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中,这时 Routing key 不起作用。
Direct
这种类型的Exchange路由规则也很简单,它会把消息路由到那些 binding key 与 routing key完全匹配的Queue中。
在这个设置中,我们可以看到两个队列Q1、Q2直接绑定到了交换器X上。 第一个队列用绑定key橙色(orange)绑定,第二个队列有两个绑定,一个绑定key为黑色(black),另一个为绿色(green)。
在这种设置中,通过路由键橙色发布到交换器的消息将被路由到队列Q1。 带有黑色或绿色的路由键的消息将进入Q2。 所有其他消息将被丢弃。
在上面列子中,routingKey=”error”的消息发送Exchange后,Exchange会将消息路由到Queue1(amqp.gen-S9b…,这是由RabbitMQ自动生成的Queue名称)和Queue2(amqp.gen-Agl…);如果routingKey=”info”或routingKey=”warning”的消息发到Exchange,Exchange只会将消息路由到Queue2。 所有其他消息将被丢弃。
Topic
这种类型的Exchange的路由规则支持 binding key 和 routing key 的模糊匹配,会把消息路由到满足条件的Queue。 binding key 中可以存在两种特殊字符 *与 #,用于做模糊匹配,其中 * 用于匹配一个单词,# 用于匹配0个或多个单词,单词以符号“.”为分隔符。
以上图中的配置为例,routingKey=”quick.orange.rabbit”的消息会同时路由到Q1与Q2,routingKey=”lazy.orange.fox”的消息会路由到Q1与Q2,routingKey=”lazy.brown.fox”的消息会路由到Q2,routingKey=”lazy.pink.rabbit”的消息会路由到Q2(只会投递给Q2一次,虽然这个routingKey与Q2的两个bindingKey都匹配);routingKey=”quick.brown.fox”、routingKey=”orange”、routingKey=”quick.orange.male.rabbit”的消息将会被丢弃,因为它们没有匹配任何bindingKey。
Headers
这种类型的Exchange不依赖于 routing key 与 binding key 的匹配规则来路由消息,而是根据发送的消息内容中的 headers 属性进行匹配。
RPC
MQ本身是基于异步的消息处理,前面的示例中所有的生产者(P)将消息发送到RabbitMQ后不会知道消费者(C)处理成功或者失败(甚至连有没有消费者来处理这条消息都不知道)。
但实际的应用场景中,我们很可能需要一些同步处理,需要同步等待服务端将我的消息处理完成后再进行下一步处理。这相当于RPC(Remote Procedure Call,远程过程调用)。在RabbitMQ中也支持RPC。
RabbitMQ中实现RPC的机制
1,客户端发送请求(消息)时,在消息的属性中(MessageProperties,在AMQP协议中定义了14中properties,这些属性会随着消息一起发送)设置两个值replyTo(一个Queue名称,用于告诉服务器处理完成后将通知我的消息发送到这个Queue中)和correlationId(此次请求的标识号,服务器处理完成后需要将此属性返还,客户端将根据这个id了解哪条请求被成功执行了或执行失败)
2,服务器端收到消息并处理
3,服务器端处理完消息后,将生成一条应答消息到replyTo指定的Queue,同时带上correlationId属性
客户端之前已订阅replyTo指定的Queue,从中收到服务器的应答消息后,根据其中的correlationId属性分析哪条请求被执行了,根据执行结果进行后续业务处理
PHP-RabbitMQ的使用方法
基本流程图
如果exchange没有绑定queue,则消息将被丢弃
如果创建exchange,queue,并且已经绑定了,则可以直接使用
为了防止脚本出问题,可以配合supervisor或者circus
安装
composer require php-amqplib/php-amqplib
使用
1.连接RabbitMQ服务器
2.开始一个新的 channel
3.新建一个exchange
4.新建一个queue
5.绑定queue和exchange
6.发布一个消息
7.建立一个消费者并注册一个回调函数
8.监听数据
新建连接和channel
<?php
require "./vendor/autoload.php";
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
$host = "192.168.110.134";
$port = 5672;
$user = "test";
$pass = "test";
$vhost = "/";
try{
$connection = new AMQPStreamConnection($host, $port, $user, $pass, $vhost);
}catch (Exception $e){
echo 'Caught exception: ', $e->getMessage(), "\n";die;
}
$channel = $connection->channel();
新建一个exchange
<?php
/*
name: $exchange
type: fanout
passive: false // don't check is an exchange with the same name exists
durable: false // the exchange won't survive server restarts
auto_delete: true //the exchange will be deleted once the channel is closed.
*/
try{
$name = 'example_direct_exchange';
$type = "direct";
$passive = false;
$durable = true;
$auto_delete = true;
$channel->exchange_declare($name, $type, $passive, $durable, $auto_delete);
}catch (Exception $e){
echo 'Caught exception: ', $e->getMessage(), "\n";die;
}
参数 name
exchange名称
参数 type
exchange类型
fanout 是广播类型的消息 会给所有绑定的queue发送数据
参数 passive
true
1.如果exchange已存在 则直接连接 并且不检查配置 比如已存在的exchange是fanout,新需要建立的是direct,也不会报错;
2.如果exchange不存在 则直接报错
false
1.如果exchange不存在 则创建新的exchange
2.如果exchange已存在 则判断配置是否相同。如果配置不相同 则直接报错。比如已存在的exchange是fanout,新需要建立的是direct,会报错。
参数 auto_delete
true
当最后一个消费者取消订阅之后 exchange会被自动删除 一般用于临时exchange
新建一个queue
<?php
/*
name: $queue // should be unique in fanout exchange.
passive: false // don't check if a queue with the same name exists
durable: false // the queue will not survive server restarts
exclusive: false // the queue might be accessed by other channels
auto_delete: true //the queue will be deleted once the channel is closed.
*/
$queue1 = 'example_direct_queue_1';
$channel->queue_declare($queue1, false, true, false, false);
将queue和exchange绑定
$queue1 = 'example_direct_queue_1';
$exchange_name = 'example_direct_exchange';
$channel->queue_bind($queue1, $exchange_name);
发布一个消息
<?php
$exchange_name = 'example_direct_exchange';
$messageBody = array(
'example_direct_value'=>date('Y-m-d H:i:s'),
);
$message = new AMQPMessage(json_encode($messageBody));
$channel->basic_publish($message, $exchange_name);
建立一个消费者并注册一个回调函数
<?php
/*
queue: Queue from where to get the messages
consumer_tag: Consumer identifier
no_local: Don't receive messages published by this consumer.
no_ack: Tells the server if the consumer will acknowledge the messages.
exclusive: Request exclusive consumer access, meaning only this consumer can access the queue
nowait:
callback: A PHP Callback
*/
$consumerTag = 'consumer';
$queue = 'example_direct_queue_1';
$channel->basic_consume($queue, "", false, false, false, false,function($msg){
$message = json_decode($msg->body, true);
file_put_contents("./mq.log", $message,FILE_APPEND);
$msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
});
参数no_ack
true
消息只有在返回一个ack之后,才会被删除
false
消息被取出之后 会被立即删除
监听数据
<?php
try {
while (count($channel->callbacks)) {
$channel->wait();
}
}
catch(\PhpAmqpLib\Exception\AMQPTimeoutException $e){
$channel->close();
$channel->close();
}