MQ

135 阅读8分钟

重点掌握:

  • 核心部分
  • 高级部分
  • 集群部分

核心部分:

  1. Hello World(简单模式)
  2. Work queues(工作模式)
  3. Publish/Subscribe(发布订阅模式)
  4. Routing(路由模式)
  5. Topics(通配符模式)
  6. Publisher Confirms(发布确认模式)

高级部分:

  1. 死信队列
  2. 延迟队列
  3. 发布确认高级(发布确认,回退消息,备份交换机)
  4. 幂等性
  5. 优先级队列
  6. 惰性队列

集群部分:

  1. Clustering(搭建MQ集群)
  2. 镜像队列(为了数据库丢失)
  3. Haproxy+Keepalive实现高可用负载均衡
  4. Federation Exchange(联邦交换机)
  5. Federation Queue(联邦队列)
  6. Shovel(同步数据)

什么是MQ

本质是一个队列,FIFO先入先出原则,只不过队列中存放的内容是message而已,还是一个种跨进程的通信机制,用于上下游传递消息,在互联网架构中,MQ是一种非常普遍的上下游“逻辑解耦+物理解耦”的消息通信服务。使用了MQ之后,消息发送上游只需要依赖MQ,不用依赖其他服务

为什么要使用MQ

流量消峰

举个例子,如果订单系统最多能处理一万次,超过了可能崩溃。所以要超过一万次就要使用MQ来做缓冲,虽然会导致速度慢,但总比崩溃好

应用解耦

以电商为例,应用中有订单系统,库存系统,物流系统,支付系统。用户创建订单后,如果耦合的调用其系统,那么任何一个系统出现故障,都会造成下单异常。

当使用了消息队列后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几秒来修复。在这几秒钟里物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成,当物流系统恢复后,继续处理订单信息即可,用户感受不到物流系统的故障,提升系统的可用性

异步处理

有些服务间调用是异步的,例如A调用B,B需要花费很长时间执行,但是A需要知道B什么时候执行完。 那么以前有两种方法,一种是A过一段时间去调用B的查询api查询,另一种提供一个callback api,B执行完之后调用api通知A服务。这两种方式都不是很优雅。

消息队列方式:A调用B后,只需要监听B处理完成的消息,当B处理完后,发送一个消息给MQ,MQ将消息转发给A

MQ分类

  1. Kafka(大型公司用)

大数据的杀手锏,谈到大数据领域内的消息传输,则绕不开Kafka,这款为大数据而生的消息中间件,以其百万级TPS的吞吐量名声大噪,

  1. 优点:性能卓越,单机写入TPS约在百万条/秒,最大的有点,就是吞吐量高。时效性ms级可用性非常高,Kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用,消费者采用Pull方式获取消息,消息有序,通过控制能够保证所有消息被消费且仅被消费一次;有优秀的第三方Kaf Web管理界面Kafka-Mnager;在日志领域比较成熟,被多家公司和开源项目使用,功能支持:功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用
  2. 缺点:Kafka单机超过64个队列/分区,Load会发生明显的飙高现象,队列越多,load越高,发送消息响应时间变长,使用短轮询方式,实时性取决于轮询间隔时间,消费失败不支持重试,支持消息顺序,但一台代理宕机后,就会产生消息乱序,社区更新较慢
  1. RabbitMQ(中小型用)

2007年发布,是一个在AMQP(高级消息队列协议)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一

  1. 优点:由于erlang语言的高并发特性,性能较好;吞吐量到万级,MQ功能比较完善,支持多种语言,开源提供的管理界面非常棒,社区活跃度高
  2. 缺点:商业版需要收费,学习成本高

RabbitMQ

四大核心:生产者,MQ(交换机,队列),消费者

RabbitMQ基本使用手册

里面有各种语言的使用,下面做PHP的代码

1、Hello World(简单模式)

composer中引入,手册里面有

image.png

生产者代码:

<?php

declare(strict_types=1);

namespace app\controller;


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

class Login
{
    public function index()
    {   
        // 1、创建连接对象
        $host = '127.0.0.1'; // 主机地址
        $port = 5672; // 端口
        $user = 'guest'; // 用户名
        $pass = 'guest'; // 密码
        $vhost = '/'; // 虚拟机
        $connection = new AMQPStreamConnection($host, $port, $user, $pass, $vhost);

        // 2、创建通道
        $channel = $connection->channel();

        // 3、创建队列
        $queue = 'hello'; // 队列名称
        $durable = false; // 持久化, 消息队列在重启后是否需要重新创建
        $exclusive = false; // 排他, 队列是否为独占,只能有一个消费者监听队列,当者消费者($connection)被销毁后,队列消失
        $auto_delete = false; // 自动删除,当最后一个消费者监听队列后,队列是否自动删除
        $arguments = null; // 队列参数
        //如果没有队列就会创建,没有就不会创建
        $channel->queue_declare($queue, $durable, $exclusive, $auto_delete, $arguments);

        // 4、发送消息
        $msg = new AMQPMessage('测试ypj');// 设置需要发送的消息

        $exchange = ''; // 交换器名称,简单模式下交换机会使用默认的''
        $routing_key = 'hello'; // 路由key,交换器模式下使用,默认为''
        $mandatory = false; // 消息是否需要应答,默认为false
        $immediate = false; // 是否立即发送消息,默认为false
        // 发送消息
        $channel->basic_publish($msg, $exchange, $routing_key,$mandatory,$immediate);
        echo '[x]Sent Hello World!';

        // 5、释放资源关闭连接
        $channel->close();
        $connection->close();
    }
}


消费者代码:

<?php
namespace app\controller;
use PhpAmqpLib\Connection\AMQPStreamConnection;
class re
{
  public function index(){

    $connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
    $channel = $connection->channel();
    $channel->queue_declare('hello', false, false, false, false);
    dump( " [x] waiting 'Hello World!'\n");
    $callback = function($msg){
      dump(" [x] Received ", $msg->body, "\n") ;
    };
    $channel->basic_consume('hello', '', false, true, false, false, $callback);
    while($channel->is_consuming()){
      $channel->wait();
    }
  }
}

2、Work queues(工作模式)

image.png

与简单模式相比,多了一个消费者,多个消费者共同使用同一个队列中的消息

应用场景:对于任务过重或任务较多的情况使用工作队列可以提高任务处理的速度

代码和简单模式一样的,这里就不做展示,不会看手册

3、Publish/Subscribe(发布订阅模式)

image.png

Exchange(X):就是交换机,一方面,接收生产者发送的消息,另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或者将消息丢弃。到底如何操作,取决于Exchange的类型。

Exchange有以下3种类型:

  1. Fanout:广播,将消息交给所有绑定到交换机的队列
  2. Direct:定向,将消息交给符合指定routing key的队列
  3. Topic:通配符,将消息交给routing pattern(路由模式)的队列

Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失

生产者:

<?php
namespace app\controller;


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

class Login
{
    public function index()
    {   
        // 1、创建连接对象
        $host = '127.0.0.1'; // 主机地址
        $port = 5672; // 端口
        $user = 'guest'; // 用户名
        $pass = 'guest'; // 密码
        $vhost = '/'; // 虚拟机
        $connection = new AMQPStreamConnection($host, $port, $user, $pass, $vhost);

        // 2、创建通道
        $channel = $connection->channel();

        // 3、创建交换机
        $channel->exchange_declare('logs', 'fanout', false, false, false);

        // 4、创建队列
        $queue1Name = 'queue1';
        $queue2Name = 'queue2';
        $channel->queue_declare($queue1Name, false, true, false, false);
        $channel->queue_declare($queue2Name, false, true, false, false);
        // 5、绑定队列和交换机
        $channel->queue_bind($queue1Name, 'logs', '');
        $channel->queue_bind($queue2Name, 'logs', '');
        // 6、发送消息
        $msg = new AMQPMessage('日志信息');
        $channel->basic_publish($msg , 'logs');// 交换机名称
        // $channel->basic_publish($msg , '',$queue1Name);// 不写交换机名称,写队列名称,相当于发送到队列
        // 7、关闭通道和连接
        $channel->close();
        $connection->close();
    }
}

消费者:

<?php
namespace app\controller;
use PhpAmqpLib\Connection\AMQPStreamConnection;
class re
{
  public function index(){

    $connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
    $channel = $connection->channel();
    // 队列名称
    $queue1Name = 'queue1';
    $queue2Name = 'queue2';

    $callback = function($msg){
      dump(" [x] Received ", $msg->body, "\n");
    };
    $channel->basic_consume($queue1Name, '', false, true, false, false, $callback);
    while($channel->is_consuming()){
      $channel->wait();
    }
  }
}

4、Routing(路由模式)

image.png

不同级别的信息,去到不同的队列

生产者:

<?php
namespace app\controller;


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

class Login
{
    public function index()
    {   
        // 1、创建连接对象
        $host = '127.0.0.1'; // 主机地址
        $port = 5672; // 端口
        $user = 'guest'; // 用户名
        $pass = 'guest'; // 密码
        $vhost = '/'; // 虚拟机
        $connection = new AMQPStreamConnection($host, $port, $user, $pass, $vhost);

        // 2、创建通道
        $channel = $connection->channel();

        // 3、创建交换机 注意路由模式的类型
        $channel->exchange_declare('test_direct', 'direct', false, false, false);

        // 4、创建队列
        $queue1Name = 'queue1_direct';
        $queue2Name = 'queue2_direct';
        $channel->queue_declare($queue1Name, false, true, false, false);
        $channel->queue_declare($queue2Name, false, true, false, false);
        // 5、绑定队列和交换机
            // 队列1绑定
        $channel->queue_bind($queue1Name, 'test_direct', 'error');
            // 队列2绑定
        $channel->queue_bind($queue2Name, 'test_direct', 'info');
        // $channel->queue_bind($queue2Name, 'test_direct', 'error');
        $channel->queue_bind($queue2Name, 'test_direct', 'warning');
        // 6、发送消息
        $msg = new AMQPMessage('日志信息');
        $channel->basic_publish($msg , 'test_direct','info');// 交换机名称 
        // 级别为info的时候只会走队列2
   
        // 7、关闭通道和连接
        $channel->close();
        $connection->close();
    }
}

消费者:根据图 需要多个消费者来处理不同级别的问题

<?php
namespace app\controller;
use PhpAmqpLib\Connection\AMQPStreamConnection;
class re
{
  
  public function index(){

    $connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
    $channel = $connection->channel();
    // 队列名称
    $queue1Name = 'queue1_direct';
        // $queue2Name = 'queue2_direct';

    $callback = function($msg){
      dump(" [x] Received ", $msg->body, "\n");
    };
    $channel->basic_consume($queue1Name, '', false, true, false, false, $callback);
    while($channel->is_consuming()){
      $channel->wait();
    }
  }
}

5、topic(通配符模式)

image.png

生产者:

<?php
namespace app\controller;


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

class Login
{
    public function index()
    {   
        // 1、创建连接对象
        $host = '127.0.0.1'; // 主机地址
        $port = 5672; // 端口
        $user = 'guest'; // 用户名
        $pass = 'guest'; // 密码
        $vhost = '/'; // 虚拟机
        $connection = new AMQPStreamConnection($host, $port, $user, $pass, $vhost);

        // 2、创建通道
        $channel = $connection->channel();

        // 3、创建交换机 注意路由模式的类型
        $channel->exchange_declare('test_topics', 'topic', false, false, false);

        // 4、创建队列
        $queue1Name = 'queue1_topics';
        $queue2Name = 'queue2_topics';
        $channel->queue_declare($queue1Name, false, true, false, false);
        $channel->queue_declare($queue2Name, false, true, false, false);
        // 5、绑定队列和交换机
            // 所有error级别,所有order级别
        $channel->queue_bind($queue1Name, 'test_topics','#.error');
        // $channel->queue_bind($queue1Name, 'test_topics','order.*');
        $channel->queue_bind($queue2Name, 'test_topics','*.*');
        // 6、发送消息
        $msg = new AMQPMessage('日志信息');
        $channel->basic_publish($msg , 'test_topics','goods.info');// 交换机名称
   
        // 7、关闭通道和连接
        $channel->close();
        $connection->close();
    }
}

消费者:和前面的写法没有区别

此方法可以实现Pub/Sub 与 Routing的功能,只是在配置routing key的时候可以使用通配符,显得更加灵活

*:是一个单词

#:是0到多个单词

RabbitMQ高级特性

1、消息的可靠投递

在使用RabbitMQ的时候,作为消息发送放希望杜绝任何消息丢失或者投递失败场景。

两种方法来控制消息的可靠性投递:

  1. confirm 确认模式
  2. return 退回模式

RabbitMQ整个消息投递的路径为:

producer->rabbitmq broker->exchange->queue->consumer

confirm 确认模式

  • 消息从producer到exchange会返回一个confirmCallback回调函数
<?php

namespace app\controller;


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

class Login
{
    public function index()
    {
        // 1、创建连接对象
        $host = '127.0.0.1'; // 主机地址
        $port = 5672; // 端口
        $user = 'guest'; // 用户名
        $pass = 'guest'; // 密码
        $vhost = '/'; // 虚拟机
        $connection = new AMQPStreamConnection($host, $port, $user, $pass, $vhost);

        // 2、创建通道
        $channel = $connection->channel();

        // 3、创建交换机 注意路由模式的类型
        $channel->exchange_declare('test_confirm', AMQPExchangeType::FANOUT, false, false, false);
        // 4、设置为必须要确认
        $channel->confirm_select();
        //    推送成功
        $channel->set_ack_handler(function (AMQPMessage $message) {
            echo "ack:" . $message->body . "\n";
        });
        //    推送失败
        $channel->set_nack_handler (function (AMQPMessage $message) {
            echo "nack:" . $message->body . "\n";
        });
        //   
        // 5、创建队列
        $queue1Name = 'test_queue_confirm';
        $channel->queue_declare($queue1Name, false, true, false, false);
        // 6、绑定队列和交换机

        $channel->queue_bind($queue1Name, 'test_confirm', '');

        // 7、发送消息
        $msg = new AMQPMessage('日志信息');
        $channel->basic_publish($msg, 'test_confirm', '');
        // 监听成功或失败返回结束 成功/失败 => set_ack_handler/set_nack_handler
        $channel->wait_for_pending_acks();
        // 8、关闭通道和连接
        $channel->close();
        $connection->close();
    }
}


return 退回模式

  • 消息从exchange->queue投递失败则会返回一个returnCallback
<?php

namespace app\controller;


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

class Login
{
    public function index()
    {
        // 1、创建连接对象
        $host = '127.0.0.1'; // 主机地址
        $port = 5672; // 端口
        $user = 'guest'; // 用户名
        $pass = 'guest'; // 密码
        $vhost = '/'; // 虚拟机
        $connection = new AMQPStreamConnection($host, $port, $user, $pass, $vhost);

        // 2、创建通道
        $channel = $connection->channel();

        // 3、创建交换机 注意路由模式的类型
        $channel->exchange_declare('hidden_exchange', AMQPExchangeType::TOPIC);

        $msg = new AMQPMessage('hello world');
        $wait = true;
        // 设置returnCallback函数
        $returnListener = function ($replyCode, $replyText, $exchange, $routingKey, $msg) use (&$wait) {
            echo 'return listener';
            echo $replyCode, "\n";
            echo $replyText, "\n";
            echo $exchange, "\n";
            echo $routingKey, "\n";
            echo $msg->body, "\n";
            $wait = false;
        };
        $channel->set_return_listener($returnListener);
        // 参数 mandatory:true,当发送消息不可达时,会执行ReturnListener ,如果为false则会删除该消息
        // 代码到这里的情况是,只有交换机但没有队列,所以消息会返回给生产者,但开启了mandatory:true,因此会走returnCallback函数
        $channel->basic_publish($msg, 'hidden_exchange', 'rkey', true);

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

2、消费端限流

image.png

<?php

namespace app\controller;

use PhpAmqpLib\Connection\AMQPStreamConnection;

class re
{

  public function index()
  {

    $connection = new AMQPStreamConnection('localhost', 5672, 'guest', 'guest');
    $channel = $connection->channel();
    // 队列名称
    $queue1Name = 'queue2_topics';

    $channel->queue_declare($queue1Name, false, true, false, false);
    // prefetchSize单条消息的大小,如果设置为0,则表示不限制。
    // prefetchCount一次最多能处理多少条消息
    // global是否将上面设置为true应用于channel级别还是取false代表Con级别
    $prefetchSize = 0;
    $prefetchCount = 2;
    $global = 0;
    $channel->basic_qos($prefetchSize, $prefetchCount, $global);

    $callback = function($msg){
      // 消费完后进行应答,告诉rabbitmq可以发送下一条消息了
      $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
    };

    // 设置autoAck为false,表示手动ack
    $channel->basic_consume($queue1Name, '', false, false, false, false, $callback);
    while ($channel->is_consuming()) {
      $channel->wait();
    }
  }
}

3、TTL

TTl全称Time To Live(存活时间/过期时间)

当消息到达存活时间后,还没有被消费,会被自动清除

RabbitMQ可以对消息设置过期时间,也可以对整个队列设置过期时间

4、死信队列