RabbitMQ的工作模式
RabbitMQ的六种工作模式(含PHP代码实现)
接着上一篇文章《RabbitMQ入门》,我们再来看下RabbitMQ的工作模式有哪些。
1、简单队列模式(simple queue)-最简单的收发模式
1)只包含一个生产者和一个消费者
2)生产者将消息发送到队列中,消费者从队列中接收消息
场景:有一个OA系统,用户通过接收手机验证码进行注册,页面上点击获取验证码后,将验证码放到消息队列,然后短信服务从队列中获取到验证码,并发送给用户。
工作过程:
消息的消费者(consumer) 监听消息队列,如果队列中有消息,就消费掉,消息被拿走后,自动从队列中删除。
可能存在的问题:
消息可能没有被消费者正确处理,已经从队列中消失了,造成消息的丢失。
解决办法:
这里可以设置成手动的ack,但如果设置成手动ack,处理完后要及时发送ack消息给队列,否则会造成内存溢出。
扩展一下:消息确认-自动应答和手动应答
noack:true 自动应答 ,false(手动应答) 默认为false-关闭
noack=false时,RabbitMQ会等待消费者显式发回ack信号后才从内存(和磁盘,如果是持久化消息的话)中移去消息。否则,RabbitMQ会在队列中消息被消费后立即删除它。
注意:
生产者将消息投递到Queue中,实际上这在RabbitMQ中这种事情永远都不会发生。
实际的情况是:当你手动创建一个队列时,后台会自动将这个队列绑定到一个名称为空,类型为default的交换机(exchange)上,绑定 RoutingKey 与队列名称相同。有了这个默认的交换机和绑定,使我们只关心队列这一层即可,这个比较适合做一些简单的应用。
下面我们从代码层面来感受一下:
1)生产端
1 <?php 2 3 namespace console\controllers; 4 5 use PhpAmqpLib\Connection\AMQPStreamConnection; 6 use PhpAmqpLib\Message\AMQPMessage; 7 use yii\console\Controller; 8 9 class SendController extends Controller 10 { 11 public function actionIndex() 12 { 13 // RabbitMQ: 简单的收发模式 14 15 // 打开一个连接和通道 16 $connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest'); 17 $channel = $connection->channel(); 18 // 声明一个队列 19 $channel->queue_declare('hxq', false, false, false, false); 20 21 // 向队列发布消息 22 $msg = new AMQPMessage('Hello World!'); 23 24 // 注意这个路由key一定要设置跟队列匹配,交换机名称为空,类型为default 25 $channel->basic_publish($msg, '', 'hxq'); 26 echo "[x] Sent 'Hello World!'\n"; 27 28 // 关闭通道和连接 29 $channel->close(); 30 $connection->close(); 31 } 32 }
2)消费端
1 <?php 2 3 namespace console\controllers; 4 5 use PhpAmqpLib\Connection\AMQPStreamConnection; 6 use yii\console\Controller; 7 8 class ReceiveController extends Controller 9 { 10 public function actionIndex() 11 { 12 // RabbitMQ: 简单的收发模式 13 14 // 打开一个连接和通道 15 $connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest'); 16 $channel = $connection->channel(); 17 // 声明一个队列 18 $channel->queue_declare('hxq', false, false, false, false); 19 echo '[*] Waiting for messages. To exit press CTRL+C', "\n"; 20 21 // 接收消息进行处理的回调函数 22 $callback = function ($msg) { 23 echo "[x] Received ", $msg->body, "\n"; 24 }; 25 26 // basic_consume是一个阻塞函数,在接收消息的时候调用$callback函数 27 $channel->basic_consume('hxq', '', false, true, false, false, $callback); 28 while ($channel->is_consuming()) { 29 $channel->wait(); 30 } 31 32 // 关闭通道和连接 33 $channel->close(); 34 $connection->close(); 35 } 36 }
下面我们开两个命令窗口,模拟发送端和消费端:
a) 消费者在队列端监听:
b)发送端发送消息
c)消费者获取到消息,进行消费
2、工作队列模式(work Queues)-资源的竞争
在simple模式下只有一个生产者和消费者,当生产者生产消息的速度大于消费者的消费速度时,我们可以添加一个或多个消费者来加快消费速度,这种在simple模式下增加消费者的模式,称为work模式
工作队列是为了避免等待一些占用大量资源、时间的操作。当我们把任务(Task)当作消息发送到队列中,一个运行在后台的工作者(worker)进程就会取出任务然后处理。当你运行多个工作者(workers),任务就会在它们之间共享。
这个概念在网络应用中是非常有用的,它可以在短暂的HTTP请求中处理一些复杂的任务。
特点:
1) 可以有多个消费者,但一条消息只能被一个消费者获取
2)工作队列有轮询分发(Round-Robin)和公平分发(Fair Dispatch)两种模式
场景:有一个电商平台,有两个订单服务,用户下单的时候,任意一个订单服务消费用户的下单请求生成订单即可。不用两个订单服务同时消费用户的下单请求。
工作过程:
消息产生者将消息放入队列消费者可以有多个,消费者C1,消费者C2同时监听同一个队列。
C1、C2共同争抢当前的消息队列内容,,一条消息只能由一个消费者消费,这样就形成了资源竞争,谁的资源空闲大,争抢到的可能性就大,谁先拿到谁负责消费消息。
可能存在的问题:
高并发情况下,默认会产生某一个消息被多个消费者共同使用。
解决办法:
可以设置一个开关(syncronize) 保证一条消息只能被一个消费者使用。
注意:
上图中虽然没有画出交换机的部分,但是原理同simple queue中阐述的一样,不再赘述。
下面是参考代码:
1)生产端
1 <?php 2 3 namespace console\controllers; 4 5 use PhpAmqpLib\Connection\AMQPStreamConnection; 6 use PhpAmqpLib\Message\AMQPMessage; 7 use yii\console\Controller; 8 9 class SendController extends Controller 10 { 11 public function actionIndex2($argv) 12 { 13 // RabbitMQ: 工作队列-资源的竞争 14 // 打开一个连接和通道 15 $connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest'); 16 $channel = $connection->channel(); 17 // 声明一个队列,第三个参数durable(是否消息持久化):true是 false否 18 $channel->queue_declare('task_queue', false, true, false, false); 19 20 21 if (empty($argv)) { 22 $argv = "Hello World!"; 23 } 24 // 向队列发布消息 25 $msg = new AMQPMessage($argv, ['delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT]); 26 27 // 注意这个路由key一定要设置跟队列匹配 28 $channel->basic_publish($msg, '', 'task_queue'); 29 echo "[x] Sent ", $argv, "\n"; 30 31 // 关闭通道和连接 32 $channel->close(); 33 $connection->close(); 34 } 35 }
2) 消费端
1 <?php 2 3 namespace console\controllers; 4 5 use PhpAmqpLib\Connection\AMQPStreamConnection; 6 use yii\console\Controller; 7 8 class ReceiveController extends Controller 9 { 10 public function actionIndex2() 11 { 12 // RabbitMQ: 工作队列-资源的竞争 13 14 // 打开一个连接和通道,声明一个队列 15 $connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest'); 16 $channel = $connection->channel(); 17 // 第三个参数durable为true 消息持久化 18 $channel->queue_declare('task_queue', false, true, false, false); 19 echo '[*] Waiting for messages. To exit press CTRL+C', "\n"; 20 21 // 接收消息进行处理的回调函数 22 $callback = function ($msg) { 23 echo "[x] Received ", $msg->body, "\n"; 24 // 模拟耗时操作 25 sleep(substr_count($msg->body, '.')); 26 echo "[x] Done", "\n"; 27 28 // 手动ack 29 $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']); 30 31 }; 32 33 // 公平调度-只有consumer已经处理并确认了上一条message时queue才分派新的message给它,为了测试使用前后的效果,我们先把下面代码屏蔽掉,等会儿再打开 34 // $channel->basic_qos(null, 1, null); 35 36 // basic_consume第四个参数:no_ack true 自动应答 false手动应答 37 $channel->basic_consume('task_queue', '', false, false, false, false, $callback); 38 while ($channel->is_consuming()) { 39 $channel->wait(); 40 } 41 42 // 关闭通道和连接 43 $channel->close(); 44 $connection->close(); 45 } 46 }
为了模拟多个worker,我们这里开三个命令窗口,依次开local,local2作为消费端,local3为发送端,然后进行如下操作:
我们从上面图中可以发现,它仍旧没有按照我们期望的那样进行分发。比如图上有两个工作者(workers),local中工作者处理的第一条消息耗时5秒,local2中工作者处理的第一条消息耗时1秒。明明local1中的工作者还没有处理完,响应消息。然而RabbitMQ并不知道这些,它仍然一如既往的派发消息,将sleep(3)的这条消息依然发给了local。
这是因为RabbitMQ只管分发进入队列的消息,不会关心有多少消费者(consumer)没有作出响应。它盲目的把第n-th条消息发给第n-th个消费者。
公平调度
为了解决上面的问题,我们可以使用basic.qos方法,并设置prefetch_count=1。这样是告诉RabbitMQ,在同一时刻,不要发送超过1条消息给一个工作者(worker),直到它已经处理了上一条消息并且作出了响应。这样,RabbitMQ就会把消息分发给下一个空闲的工作者(worker)。
于是我们把消费者端的这段代码注释打开:
$channel->basic_qos(null, 1, null);
再测试下:
这就比较符合我们的预期了,大家可以自行测试下。
说明:保证资源竞争的代码就是这一行$channel->basic_qos(null, 1, null);如果不加这一行,我们会发现两个消费者是轮询消费消息的。
队列大小
如果所有的工作者都处于繁忙状态,那么队列就会被填满。此时需要留意这个问题,要么添加更多的工作者(workers),要么使用其他策略。
3、发布-订阅模式(Publish/SubScribe)
work模式可以将消息转到多个消费者,但每条消息只能由一个消费者获取,如果我们想一条消息可以同时给多个消费者消费呢?这时候就需要发布/订阅模式。
1)发布/订阅模式中,exchange的type为fanout。
2)生产者发送消息时,不需要指定具体的队列名,exchange会将收到的消息转发到所绑定的队列。
3)消息被exchange转到多个队列,一条消息可以被多个消费者获取。
场景:商城系统中,更新商品库存后需要通知多个缓存和多个数据库。
工作过程:
RabbitMQ把所有发送到该Exchange的消息路由到所有与它绑定的Queue中,无视binding key。一个消息可以被多个消费者消费。
注意:
如果消息发送到没有队列绑定的交换机时,消息将会消失,因为交换机没有存储消息的能力,只有队列才有存储消息的能力。
举个栗子:
为了描述这种模式,我们将会构建一个简单的日志系统。它包括两个程序——第一个程序负责发送日志消息,第二个程序负责获取消息并输出内容。在我们的这个日志系统中,所有正在运行的接收方程序都会接收消息。我们用其中一个接收者(receiver)把日志写入硬盘中,另外一个接收者(receiver)把日志输出到屏幕上。最终,日志消息被广播给所有的接收者(receivers)。
1)生产端
1 <?php 2 3 namespace console\controllers; 4 5 use PhpAmqpLib\Connection\AMQPStreamConnection; 6 use PhpAmqpLib\Message\AMQPMessage; 7 use yii\console\Controller; 8 9 class SendController extends Controller 10 { 11 public function actionIndex3($argv) 12 { 13 // RabbitMQ: 工作队列-发布/订阅(fanout) 14 $connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest'); 15 $channel = $connection->channel(); 16 // 声明交换机 17 $channel->exchange_declare('logs', 'fanout', false, false, false); 18 19 if (empty($argv)) { 20 $argv = "info: Hello World!"; 21 } 22 23 // 发送消息到我们命名为logs的交换机 24 $msg = new AMQPMessage($argv); 25 $channel->basic_publish($msg, 'logs'); 26 echo "[x] Sent ", $argv, "\n"; 27 28 // 关闭通道和连接 29 $channel->close(); 30 $connection->close(); 31 } 32 }
2)消费端
1 <?php 2 3 namespace console\controllers; 4 5 use PhpAmqpLib\Connection\AMQPStreamConnection; 6 use yii\console\Controller; 7 8 class ReceiveController extends Controller 9 { 10 public function actionIndex3() 11 { 12 // RabbitMQ: 工作队列-发布/订阅(fanout) 13 $connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest'); 14 $channel = $connection->channel(); 15 16 // 声明交换机 17 $channel->exchange_declare('logs', 'fanout', false, false, false); 18 list($queue_name, ,) = $channel->queue_declare("", false, false, true, false); 19 20 // 绑定队列到交换机 21 $channel->queue_bind($queue_name, 'logs'); 22 echo ' [*] Waiting for logs. To exit press CTRL+C', "\n"; 23 24 // 定义接收消息进行处理的回调函数 25 $callback = function ($msg) { 26 echo "[x] Received ", $msg->body, "\n"; 27 }; 28 29 $channel->basic_consume($queue_name, '', false, true, false, false, $callback); 30 31 while (count($channel->callbacks)) { 32 $channel->wait(); 33 } 34 35 // 关闭通道和连接 36 $channel->close(); 37 $connection->close(); 38 } 39 }
我们来模拟实现一个日志系统,分别开两个窗口来作为消费端监听,其中一个将监听消息写入日志,另外一个直接命令窗口输出,如下图:
再开一个窗口来发送消息:
4、路由模式(routing)
前面几种模式,消息的目标队列无法由生产者指定,而在路由模式下,消息的目标队列,可以由生产者指定。
- 路由模式下Exchange的type为direct。
- 消息的目标队列可以由生产者按照routingKey规则指定。
- 消费者通过BindingKey绑定自己所关心的队列。
- 一条消息可以被多个消费者获取。
- 只有RoutingKey与BidingKey相匹配的队列才会收到消息。
场景:商城系统中,新增加了一个商品,实时性不是很高,只需要添加到数据库即可,不用刷新缓存。
工作过程:生产者将消息发送到direct交换机,它会把消息路由到那些binding key 与 routing key 完全匹配的queue中。在相应队列监听的消费者才能消费消息。这样就能实现消费者有选择的去消费消息。
说明: 如果routing key为black或者green,那么交换机会将消息路由到队列Q2中。
说明: 如果routing key为black,那么交换机会将消息路由到队列Q1和队列Q2中。
举个栗子:
我们的日志系统广播所有的消息给所有的消费者(consumers)。我们打算扩展它,使其基于日志的严重程度进行消息过滤。例如我们也许只是希望将比较严重的错误(error)日志写入磁盘,以免在警告(warning)或者信息(info)日志上浪费磁盘空间。
下面参考代码:
1)生产端
1 <?php 2 3 namespace console\controllers; 4 5 use PhpAmqpLib\Connection\AMQPStreamConnection; 6 use PhpAmqpLib\Message\AMQPMessage; 7 use yii\console\Controller; 8 9 class SendController extends Controller 10 { 11 public function actionIndex4($argv) 12 { 13 // RabbitMQ: 路由模式 14 $connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest'); 15 $channel = $connection->channel(); 16 17 // 声明交换机 18 $channel->exchange_declare('direct_logs', 'direct', false, false, false); 19 if (strpos($argv, '/') !== false) { 20 $arr = explode('/', $argv); 21 // routing_key 22 $severity = $arr[0]; 23 // 消息内容 24 $data = $arr[1]; 25 } else { 26 echo '参数错误'; 27 return; 28 } 29 // 发送消息到我们命名为direct_logs的交换机 30 $msg = new AMQPMessage($data); 31 $channel->basic_publish($msg, 'direct_logs', $severity); 32 echo "[x] Sent ", $data, "\n"; 33 34 // 关闭通道和连接 35 $channel->close(); 36 $connection->close(); 37 } 38 }
2)消费端
1 <?php 2 3 namespace console\controllers; 4 5 use PhpAmqpLib\Connection\AMQPStreamConnection; 6 use yii\console\Controller; 7 8 class ReceiveController extends Controller 9 { 10 public function actionIndex4($argv) 11 { 12 // RabbitMQ: 路由模式 13 14 $connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest'); 15 $channel = $connection->channel(); 16 17 // 声明交换机 18 $channel->exchange_declare('direct_logs', 'direct', false, false, false); 19 list($queue_name, ,) = $channel->queue_declare("", false, false, true, false); 20 21 // 为我们感兴趣的每个严重级别分别创建一个新的绑定 22 if (strpos($argv, '/') !== false) { 23 $severities = explode('/', $argv); 24 foreach ($severities as $severity) { 25 // 第三个参数为routing_key 26 $channel->queue_bind($queue_name, 'direct_logs', $severity); 27 } 28 } else { 29 $severity = $argv; 30 $channel->queue_bind($queue_name, 'direct_logs', $severity); 31 } 32 33 echo ' [*] Waiting for logs. To exit press CTRL+C', "\n"; 34 35 // 接收消息进行处理的回调函数 36 $callback = function ($msg) { 37 echo ' [x] ', $msg->delivery_info['routing_key'], ':', $msg->body, "\n"; 38 }; 39 $channel->basic_consume($queue_name, '', false, true, false, false, $callback); 40 while (count($channel->callbacks)) { 41 $channel->wait(); 42 } 43 44 // 关闭通道和连接 45 $channel->close(); 46 $connection->close(); 47 } 48 }
消费端监听消息:
生产端发送消息:
5、主题模式(Topic) - 路由模式的一种
主题模式是在路由模式的基础上,将路由键和某模式进行匹配。其中#表示匹配零个或者多个词,*表示匹配一个词,单词之间用英文句点"."隔开,消费者可以通过某种模式的BindKey来达到订阅某个主题消息的目的。
特点:
- 主题模式下Exchange的type取值为topic。
- 一条消息可以被多个消费者获取。
主题交换机是很强大的,它可以表现出跟其他交换机类似的行为。
1) 当一个队列的绑定键为 "#"(井号) 的时候,这个队列将会无视消息的路由键,接收所有的消息。
2)当 * (星号) 和 # (井号) 这两个特殊字符都未在绑定键中出现的时候,此时主题交换机就拥有的直连交换机的行为。
场景:商城系统中,新增加了一个商品,实时性不是很高,只需要添加到数据库即可,数据库包含了主数据库mysql1和从数据库mysql2的内容,不用刷新缓存。
工作过程:
交换机根据binding key的规则,使用routing Key来模糊匹配到对应的队列,由队列的监听消费者接收消息消费。如果没有匹配到相应队列,则消息被丢弃。
1)生产端
1 <?php 2 3 namespace console\controllers; 4 5 use PhpAmqpLib\Connection\AMQPStreamConnection; 6 use PhpAmqpLib\Message\AMQPMessage; 7 use yii\console\Controller; 8 9 class SendController extends Controller 10 { 11 public function actionIndex5($argv) 12 { 13 // RabbitMQ: 主题模式 14 $connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest'); 15 $channel = $connection->channel(); 16 17 // 声明交换机 18 $channel->exchange_declare('topic_logs', 'topic', false, false, false); 19 if (strpos($argv, '/') !== false) { 20 $arr = explode('/', $argv); 21 // routing_key 22 $routing_key = $arr[0]; 23 // 消息内容 24 $data = $arr[1]; 25 } else { 26 echo '参数错误'; 27 return; 28 } 29 30 // 发送消息 31 $msg = new AMQPMessage($data); 32 $channel->basic_publish($msg, 'topic_logs', $routing_key); 33 echo "[x] Sent ", $routing_key, ':', $data, "\n"; 34 35 // 关闭通道和连接 36 $channel->close(); 37 $connection->close(); 38 } 39 }
2)消费端
1 <?php 2 3 namespace console\controllers; 4 5 use PhpAmqpLib\Connection\AMQPStreamConnection; 6 use yii\console\Controller; 7 8 class ReceiveController extends Controller 9 { 10 public function actionIndex5($argv) 11 { 12 // RabbitMQ: 主题模式 13 $connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest'); 14 $channel = $connection->channel(); 15 16 // 声明交换机 17 $channel->exchange_declare('topic_logs', 'topic', false, false, false); 18 list($queue_name, ,) = $channel->queue_declare("", false, false, true, false); 19 20 // 多个队列和这个名称为topic_logs的交换机绑定 21 if (strpos($argv, '/') !== false) { 22 $binding_keys = explode('/', $argv); 23 foreach ($binding_keys as $binding_key) { 24 // 第三个参数为routing_key 25 $channel->queue_bind($queue_name, 'topic_logs', $binding_key); 26 } 27 } else { 28 $binding_key = $argv; 29 $channel->queue_bind($queue_name, 'topic_logs', $binding_key); 30 } 31 32 echo ' [*] Waiting for logs. To exit press CTRL+C', "\n"; 33 34 // 接收消息进行处理的回调函数 35 $callback = function ($msg) { 36 echo ' [x] ', $msg->delivery_info['routing_key'], ':', $msg->body, "\n"; 37 }; 38 $channel->basic_consume($queue_name, '', false, true, false, false, $callback); 39 while (count($channel->callbacks)) { 40 $channel->wait(); 41 } 42 43 // 关闭通道和连接 44 $channel->close(); 45 $connection->close(); 46 } 47 48 }
我们分别开4个窗口,监听消息
发送消息后
6、RPC模式- 远程过程调用
这里面有两个重要的概念:
1) replyTo: 存储回调队列的名称
2) correlationId: 唯一标识本次的请求,主要用于RPC调用。
场景:订单支付
工作过程:
使用 RabbitMQ 实现 RPC,相应的角色是由生产者来作为客户端,消费者作为服务端。
但 RPC 调用一般是同步的,客户端和服务器也是紧密耦合的。即客户端通过 IP/域名和端口链接到服务器,向服务器发送请求后等待服务器返回响应信息。
但 MQ 的生产者和消费者是完全解耦的,那么如何用 MQ 实现 RPC 呢?很明显就是把 MQ 当作中间件,实现一次双向的消息传递。
客户端和服务端既是生产者也是消费者。客户端发布请求,消费响应;服务端消费请求,发布响应。
参考链接:
https://www.rabbitmq.com/getstarted.html
https://juejin.cn/post/6952757780700725256
https://www.cnblogs.com/Leo_wl/p/12439310.html