1、RabbitMQ简介

RabbitMQ是采用Erlang编写的开源消息代理服务器,基于AMQP(Advanced Message Queuing Protocol)开放协议,具有高可靠、易扩展、高可用的特点及丰富的功能特性。

RabbitMQ的特性:

  • 开源;
  • 平台和供应商无关性:AMQP具有平台和供应商无关性,而RabbitMQ实现了AMQP,且RabbitMQ提供了几乎所有语言的客户端;
  • 轻量级;
  • 插件机制:比如利用插件直接将消息写入数据库;
  • 安全:客户端可以使用SSL通信和客户端证书验证以提高安全性;可以使用插件与外部LDAP系统集成;
  • 管理界面;
  • 多种协议:RabbitMQ在支持AMQP的同时,也支持比如MQTT、Stomp和XMPP等协议;

RabbitMQ使用Erlang语言自带的进程间通信(IPC)机制实现跨节点通信。

AMQP定义了三种抽象组件用于指定消息的路由行为:

  • 交换器(Exchange):消息代理服务器中用于把消息路由到队列的组件;
  • 队列(Queue):用来存储消息的数据结构,位于硬盘或内存中;
  • 绑定(Binding):一套规则,用于告诉交换器消息应该被存储到哪个队列;

消息中间件一般有两种传递模式:

  • 点对点模式:基于队列,队列使得消息可以异步传输
  • 发布订阅模式:基于主题,适用于一对多广播

消息中间件作用:

  • 解耦
  • (冗余)存储
  • 扩展性
  • 削峰
  • 可恢复性
  • 保证顺序性
  • 缓冲
  • 异步通信

ActiveMQ是JMS的实现。

2、安装erlang

这里我介绍一下在centos上通过yum方式安装erlang的方法。

2.1、添加yum源

在 目录 /etc/yum.repos.d/ 下存储了yum常用的源,这里我们自己建立一个,使用 vi 指令来创建一个rabbitmq_erlang.repo 文件。

vi /etc/yum.repos.d/rabbitmq_erlang.repo
在这个文件中,写入下列内容。

[rabbitmq_erlang]
name=rabbitmq_erlang
baseurl=https://packagecloud.io/rabbitmq/erlang/el/7/$basearch
repo_gpgcheck=1
gpgcheck=0
enabled=1
gpgkey=https://packagecloud.io/rabbitmq/erlang/gpgkey
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300
[rabbitmq_erlang-source]
name=rabbitmq_erlang-source
baseurl=https://packagecloud.io/rabbitmq/erlang/el/7/SRPMS
repo_gpgcheck=1
gpgcheck=0
enabled=1
gpgkey=https://packagecloud.io/rabbitmq/erlang/gpgkey
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
metadata_expire=300

2.2、安装

yum install -y erlang

2.3、验证是否安装成功

erl -version

2.4、rpm方式安装

如果2.2的yum源不好用,可以使用下面的方法。

wget --content-disposition https://packagecloud.io/rabbitmq/erlang/packages/el/7/erlang-23.3.1-1.el7.x86_64.rpm/download.rpm
yum -y install erlang-23.3.1-1.el7.x86_64.rpm/download.rpm

更多安装方式请参考 rabbitmq - Repositories · packagecloud

3、安装RabbitMQ

wget https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.8.14/rabbitmq-server-3.8.14-1.el7.noarch.rpm
yum -y install rabbitmq-server-3.8.14-1.el7.noarch.rpm

// 出现类似:socat-1.7.3.2-2.el7.x86_64: [Errno 256] No more mirrors to try 的错误,这时需要装一个socat。下载页面:https://centos.pkgs.org/7/centos-x86_64/socat-1.7.3.2-2.el7.x86_64.rpm.html

安装好后,在/sbin目录下会有如下文件。

RabbitMQ入门教程_rabbitmq

RabbitMQ的安装目录为/usr/lib/rabbitmq/lib。

启动RabbitMQ(RabbitMQ默认的TCP端口是5672)

systemctl start rabbitmq-server

如果启动不了,可能与域名有关,需要在hosts文件中添加映射。

vi /etc/hosts
# 在hosts文件中添加映射,域名可通过hostname命令查看
主机ip 主机域名

开启后台管理

rabbitmq-plugins enable rabbitmq_management

访问 http://你的ip:15672/

默认的guest/guest用户只能通过本地主机登录,如下图所示。

RabbitMQ入门教程_rabbitmq_02

我们添加一个后台管理员用户。

rabbitmqctl add_user bobo ok
# 设置用户角色
rabbitmqctl set_user_tags bobo administrator
rabbitmqctl set_permissions -p / bobo '.*' '.*' '.*'

RabbitMQ的权限是vhost级别的,而不是用户级别的。

这里的授权命令格式为:rabbitmqctl set_permissions -p /vhost  username conf write read

  • conf:可配置权限(队列和交换器的创建及删除等)
  • write:可写权限(发布消息)
  • read:可读权限(与消息有关的操作,包括读取消息及清空整个队列等)

rabbitmqctl 工具是用来管理RabbitMQ 中间件的命令行工具。

RabbitMQ Management插件不仅提供了Web管理界面,还提供了HTTP API接口来方便调用。

rabbitmqadrnin是RabbitMQ Management 插件提供的功能,它会包装HTTP API 接口,使其调用显得更加简洁方便,比如. /rabbitmqadmin -u root -p rootl23 delete queue name=queuel。rabbitmqadmin 是需要安装的,具体安装方式请自行百度。

其它命令 。

# 查询所有用户
rabbitmqctl list_users
# 开启某个插件
rabbitmq-plugins enable xxx
# 关闭某个插件
rabbitmq-plugins disable xxx

4、RabbitMQ架构

4.1、架构图

RabbitMQ架构图如下所示。

RabbitMQ入门教程_消息队列_03

4.2、核心概念

生产者Producer:

  • 消息分为消息体和消息头,消息体也称为payload,消息头用来描述这条消息,比如交换器名称和一个路由键。

消费者Consumer:

  • 只消费消息体,消息头会在消息路由的过程中被丢弃掉。

队列Queue:

  • 队列是RabbitMQ内部对象,用于存储消息。这一点和Kafka这种消息中间件相反,Kafka将消息存储在topic(主题)这个逻辑层面,而相对应的队列逻辑只是topic实际存储文件中的位移标识;
  • 多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者进行处理,而不是每个消费者都收到所有的消息井处理;
  • RabbitMQ 不支持队列层面的广播消费(广播是交换机层面支持的);

交换器Exchange:

  • 由交换器将消息路由到一个或者多个队列中,如果路由不到,或许会返回给生产者,或许直接丢弃;

路由键RoutingKey:

  • 生产者将消息发给交换器的时候, 一般会指定一个RoutingKey表示消息的路由规则,Routing Key 需要与交换器类型和绑定键( BindingKey )联合使用才能最终生效;

绑定Binding :

  • RabbitMQ中通过绑定将交换器与队列关联起来,在绑定的时候一般会指定一个绑定键( BindingKey );
  • 在绑定多个队列到同一个交换器的时候, 这些绑定允许使用相同的BindingKey ;
  • RoutingKey与BindingKey可以看作同一个东西,绑定时使用绑定键,发送消息时使用路由键;

连接Connection与信道Channel:

  • 生产者与消费者与RabbitMQ交互时,需要打开一个TCP连接。紧接着,再在该连接上创建一个AMQP信道,每条信道会被指定一个唯一的ID,信道是虚拟连接;
  • RabbitMQ使用TCP连接复用,减少性能开销,因此需要在复用的TCP连接上建立虚拟连接信道;
  • 具体的关系是,多个线程共享一条TCP连接,每个线程有自己的信道,当信道流量不大时,TCP连接复用可以有效节省TCP连接资源,但当信道流量大时,多个线程共用一个TCP连接就会产生性能瓶颈;

4.3、交换器

RabbitMQ常用的交换器类型有fanout 、direct、topic 、headers 这四种,AMQP 协议里还有另外两种类型:System 和自定义。

fanout扇出交换器:将消息路由到所有与该交换器绑定的队列中;

direct直连交换器:将消息路由到BindingKey和RoutingKey完全匹配的队列中;

topic主题交换器:

  • BindingKey和RoutingKey可以使用点号分隔为多个单词;
  • BindingKey可以使用#和*两种特殊字符,#用于匹配一个单词,*用于匹配多个单词(可以是0个);

headers交换器:

  • headers 类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的headers 属性进行匹配;headers 类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在;

4.4、使用建议

RabbitMQ的消息存储在队列中,交换器的使用并不真正耗费服务器的性能,而队列会。
如果要衡量RabbitMQ当前的QPS只需看队列的即可。在实际业务应用中,需要对所创建的队列的流量、内存占用及网卡占用有一个清晰的认知,预估其平均值和峰值,以便在固定硬件资源的情况下能够进行合理有效的分配。
如果业务本身在架构设计之初己经充分地预估了队列的使用情况,可以在业务程序上线之前在服务器上创建好(比如通过页面管理、RabbitMQ命令)。
预先创建好资源的好处:

  • 免去动态创建的麻烦
  • 避免因人为因素、代码Bug导致消息丢失(可以配合mandatory参数或备份交换器来提高程序的健壮性)

如果在后期运行过程中超过预定的阔值,可以根据实际情况对当前集群进行扩容或者将相应的队列迁移到其他集群
 

5、RabbitMQ Java客户端API

5.1、引入依赖

<dependency>
  <groupld>com.rabbitmq</groupld>
  <artifactld>amqp-client</artifactid>
  <version>5.7.3</version>
</dependency>

5.2、API大全

主要涉及到的API有:连接、交换器/队列的创建与绑定、发送消息、消费消息、消费消息的确认和拒绝

import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeSet;

public class RabbitMQAPI {
    private Connection conn;

    private void createConnection() throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.216.132");
        factory.setPort(5672);
        factory.setUsername("bobo");
        factory.setPassword("ok");
        // VirtualHost相当于一个相对独立的RabbitMQ服务器,每个VirtualHost之间是相互隔离的。exchange、queue、message不能互通
        // 在RabbitMQ中,权限控制是以vhost为单位的,当创建一个用户时,用户通常会被指派给至少一个vhost,且只能访问被指派的vhost内的队列、交换器和绑定关系等。
        factory.setVirtualHost("/");
        // 可以通过uri的方式连接
        // factory.setUri("amqp://bobo:ok@192.168.216.132:5672");
        this.conn = factory.newConnection();
    }

    public Channel getChannel() throws Exception {
        Channel channel = this.conn.createChannel();
        // 不要用isOpen方法
        //channel.isOpen();
        return channel;
    }

    /**
     * 交换器API
     * @param channel
     * @throws IOException
     */
    public void apiOfExchange(Channel channel) throws IOException {
        // 声明一个交换器,exchangeDeclare方法参数如下:
        // exchange:交换器名称,多次声明同一名称的交换器不会覆盖,而是抛异常;
        // type:交换器类型;
        // durable:是否持久化;持久化可以将交换器存到磁盘;
        // autoDelete:是否自动删除;自动删除的前提是至少有一个队列或者交换器与这个交换器绑定,之后所有与这个交换器绑定的队列或交换器都与此解绑才会自动删除;
        // internal:是否内置的;生产者无法直接发送消息到内置交换器中,只能通过交换器路由到交换器这种方式;
        // argument:配置其它参数;
        channel.exchangeDeclare("bobo-exchange-a", BuiltinExchangeType.DIRECT,true);

        // 不建议使用:在声明完一个交换器后,由于不需要等待服务器返回,实际服务器还并未完成交换器的创建,那么此时生产者紧接着使用这个交换器,必然会发生异常;
        // channel.exchangeDeclareNoWait();

        // 实际不创建交换器,只用于检测交换器是否存在,如果不存在则抛出异常
        channel.exchangeDeclarePassive("bobo-exchange-a");

        // 删除交换器
        channel.exchangeDelete("bobo-exchange-a");
    }

    /**
     * 队列API
     * @param channel
     * @throws IOException
     */
    public void apiOfQueue(Channel channel) throws IOException {
        // 声明一个队列。
        // 注意:生产者和消费者都能够使用queueDeclare来声明一个队列,但是如果消费者在同一个channel上订阅了另一个队列,就无法再声明队列了。必须先取消订阅,然后将channel置为传输模式之后才能声明队列
        // queueDeclare方法参数如下:
        // queue:队列名称
        // durable:是否持久化
        // exclusive:是否排它(排它队列),如果是,则该队列仅对首次声明它的连接可见,并在连接断开时自动删除,即不同connection之间不能共用,但同一connection的不同channel可共用
        // autoDelete:是否自动删除,自动删除的前提是至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时才会自动删除
        // arguments:队列的其它参数
        AMQP.Queue.DeclareOk declareOk = channel.queueDeclare();
        // 获取自动生成的队列名称,默认参数为非持久、排它、自动删除的
        String queueName = declareOk.getQueue();

        // 类似exchangeDeclareNoWait方法
        // channel.queueDeclareNoWait();

        // 类似exchangeDeclarePassive方法
        channel.queueDeclarePassive(queueName);

        // 删除队列
        channel.queueDelete(queueName);

        // 清空队列内容而不删除队列
        channel.queuePurge(queueName);
    }

    /**
     * 交换器与队列绑定API
     * @param channel
     * @throws IOException
     */
    public void apiOfBind(Channel channel) throws IOException {
        // 将队列和交换器绑定
        channel.queueBind("queueName","exchangeName","bindingkey");
        // 将队列和交换器解绑
        channel.queueUnbind("queueName","exchangeName","bindingkey");
        // 将交换器与交换器绑定
        channel.exchangeBind("exchangeName-to","exchangeName-from","bindingkey");
    }

    /**
     * 生产者API
     * @param channel
     * @throws IOException
     */
    public void apiOfProducer(Channel channel) throws IOException {
        channel.addReturnListener((replyCode, replyText, exchange, routingKey, basicProperties, body) -> {
            System.out.println("返回的消息是:"+new String(body));
        });

        // basicPublish方法参数如下:
        // exchange:交换器名称,如果设置为空字符串则消息会被发送到RabbitMQ默认的交换器中
        // routingKey:路由键
        // props:消息的基本属性集,包含14个属性成员:contentType、contentEncoding、headers(Map<String,Object>)、deliveryMode、priority、correlationid、replyTo、expiration、messageid、timestamp、type、userId、appId、clusterid
        //        props通过new AMQP.BasicProperties.Builder().contentType().contentEncoding().build()创建
        // body(byte[]):消息体payload
        // mandatory(强制性的):
        //     设置为true时,当交换器无法根据自身类型和路由键找到一个符合条件的队列时,RabbitMQ会将消息返回给生产者,可通过channel.addReturnListener监听返回的消息;
        //            如果不想用ReturnListener,则可以使用备份(备胎)交换器将未被路由的消息存储在RabbitMQ中;
        //            将B交换器声明为A交换器的备份交换器的方式如下:Map args=new HashMap();args.put("alternate-exchange","B");channel.exchangeDeclare("A","direct",args);
        //            备份交换器和普通交换器没区别,不过建议设置为fanout类型方便使用;如果为直连类型,则当路由键与绑定键不匹配时消息会丢失;
        //     设置为false时,消息直接丢弃;
        // immediate立即的:设置为true时,如果所有匹配的队列上都没有消费者,则直接将消息返还给生产者,不用将消息存入队列而等待消费者了;
        //            RabbitMQ3.0去掉了对immediate参数的支持(如果使用会报异常),RabbitMQ官方解释是:immediate参数会影响队列的性能,
        //            增加了代码复杂性,建议采用TTL和DLX的方法替代;
        channel.basicPublish("bobo-exchange-a","bind-a",null,"Hello,world!".getBytes());
    }

    /**
     * 消费者API
     * @param channel
     * @throws IOException
     */
    public void apiOfConsumer(Channel channel) throws IOException {
        // 消费者有推和拉两种模式,推模式采用consume,拉模式采用get
        // 推模式:实现com.rabbitmq.client.Consumer接口或继承com.rabbitmq.client.DefaultConsumer
        DefaultConsumer consumer = new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("收到的消息是:"+new String(body));
                String routingKey = envelope.getRoutingKey();
                String contentType = properties.getContentType();
                // 确认消息,deliveryTag可以看作消息的编号
                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        };
        // basicConsume将信道置为接收模式,直到取消队列的订阅为止,在接收模式期间,RabbitMQ会不断地推送消息给消费者,basicQos为消费者保持的最大的未确认消息数
        // basicQos很像滑动窗口,当未提交消息数达到64时,滑动窗口达到最大,之后每提交一次消息,滑动窗口向后移一步,basicQos对拉模式无效
        // 如果只想获取单条消息则使用basicGet方法,不要用basicGet结合循环来代替basicConsume,这样会严重影响性能
        channel.basicQos(64);

        // basicConsume方法参数如下:
        // queue:队列名称
        // autoAck:是否自动确认,当autoAck为false时,RabbitMQ会先给消息打上删除标记,然后等待消费者显式地回复确认信号后才从内存(或磁盘)中移除消息,因此不用担心处理消息过程中消费者进程挂掉后消息丢失的问题
        //     如果RabbitMQ一直没有收到消费者的确认信号,有两种情况:
        //         消费者断开连接:RabbitMQ会安排该消息重新进入队列(位置不变,仅仅去除了删除标记),等待投递给下一个消费者;
        //         消费者未断开连接:RabbitMQ会一直等待,不会为未确认的消息设置过期时间;
        // consumerTag:消费者标签,用来区分多个消费者
        // noLocal:设置为true则表示不能将同一个Connection中生产者发送的消息传送给这个Connection中的消费者
        // exclusive:是否排他
        // arguments:设置消费者的其他参数
        // callback:设置消费者的回调函数,用来处理RabbitMQ推送过来的消息
        channel.basicConsume("queueName",false,"consumer-tag-a",consumer);

        // 取消订阅
        // channel.basicCancel("queueName");

        // 拉模式
        GetResponse response= channel.basicGet("queueName", false);
        System.out.println("收到的消息是:"+new String(response.getBody()));
        channel.basicAck(response.getEnvelope().getDeliveryTag(),false);

        // 消费者拒绝消息
        // requeue参数:设置为true,则RabbitMQ会将这条消息重新存入队列,以便可以发送给下一个订阅的消费者;设置为false,则RabbitMQ立即会把消息从队列中移除
        channel.basicReject(response.getEnvelope().getDeliveryTag(),false);
        // 消费者批量拒绝消息
        // multiple参数:设置为true,则表示拒绝deliveryTag编号之前所有未被当前消费者确认的消息,设置为false,与basicReject方法一样
        channel.basicNack(response.getEnvelope().getDeliveryTag(),true,false);
    }

}

6、过期时间TTL

RabbitMQ可以对消息和队列设置过期时间。

6.1、设置队列的TTL

如果对一个队列设置了过期时间,那么在队列既没有任何的消费者,又没有重新声明队列的情况下,超过过期时间时该队列就会被删除。RabbitMQ重启时,持久化的队列的过期时间会被重新计算。

下面声明一个过期时间为30分钟的队列。

Map<String,Object> args =new HashMap<String,Object>() ;
args.put("x-expires",1800000);
channel.queueDeclare(queueName,durable,exclusive,autoDelete,args) ;

6.2、设置消息的TTL

方式1:设置队列的TTL,那么队列中的所有消息具有同样的TTL。

方式2:对消息本身设置TTL,如下代码所示。

channel.basicPublish("bobo-exchange-a","bindingkey-a",true,false,new AMQP.BasicProperties.Builder().expiration("30000").build(),("hello,world:").getBytes());

消息过期删除时机:

  • 对于方式一,一旦消息过期,就会立马从队列中抹去,这是因为队列中所有消息的TTL都一样,那么越早进入队列(队头)消息就越可能过期,因此只需要从队头开始扫描即可找到过期消息。
  • 对于方式二,消息过期不会马上从队列中抹去,每条消息是否过期是在即将投递到消费者之前判定的(惰性删除),如果不是惰性删除,由于队列中消息的TTL不一致,则需要顺序扫描一遍队列才能找出所有的过期消息,影响性能。

如果消息的TTL为0,除非此时消息可以立马投递给消费者,否则会丢失。这个特性可以用来替代immediate参数。

如果两个方式一起用,则以TTL短的那个为准。

当消息过期,就会变成死信,无法再被消费者消费。

7、DLX与死信队列

DLX,全称为Dead-Letter-Exchange,也叫死信交换器或死信邮箱。

消息在一个队列中变成死信(dead message)之后,它能被重新被发送到另一个交换器中,这个交换器就是DLX,绑定DLX 的队列就称之为死信队列。

消息变成死信的几种情况:

  • 消息被拒绝,井且设置requeue参数为false;
  • 消息过期(如果是惰性的则不会自动进行DLX,而是等消费到这个过期消息时才路由到DLX);
  • 队列达到最大长度;

DLX是一个普通的交换器,可以被任何队列指定,当该队列存在死信时,就会自动路由给DLX,进而再路由给死信队列。

通过下面的方式声明DLX。

//创建DLX
channel.exchangeDeclare("dlx_exchange","direct");

Map<String, Object> args = new HashMap();
args.put("x-dead-letter-exchange","dlx_exchange");
//可以为这个DLX指定路由键,如果没有特殊指定,则使用原队列的路由键
args.put("x-dead-letter-routing-key","dlx-routing-key");

//为队列myqueue添加DLX
channel.queueDeclare("myqueue",false,false,false,args);

对于RabbitMQ来说,DLX 是一个非常有用的特性。它可以处理异常情况下,消息不能够被消费者正确消费(消费者由于某种原因拒绝消息 )而被置入死信队列中的情况,后续可以通过消费这个死信队列中的内容来分析当时所遇到的异常情况,进而可以改善和优化系统。

8、延迟队列和优先级队列

利用TTL和DLX,可以实现延迟队列。

假设TTL为10秒,那当消息过了10秒后还没有被消费,就会进入DLX,进而进入死信队列。这里的死信队列就相当于延迟时间为10秒的延迟队列。

优先级队列如下所示。

Map<String, Object> args = new HashMap();
// 最大优先级设置为10,最低优先级默认为0。优先级只有当队列中的消息有堆积才有意义
args.put("x-max-priority", 10);
channel.queueDeclare("queue.priority",true,false,false,args) ;

// 生产者发消息时指定消息的优先级
channel.basicPublish("exchange_priority","queue.priority",new AMQP.BasicProperties.Builder().priority(5).build(),"message".getBytes());

9、实现RPC

RabbitMQ入门教程_rabbitmq_04

如上图所示,可以通过replyTo方法创建一个回调队列,以及通过correlationId方法将请求与响应关联起来。

String callbackQueueName = channel.queueDeclare() . getQueue();
channel.basicPublish("","rpc_queue",new AMQP.BasicProperties.Builder().replyTo(callbackQueueName).correlationId("abcdefg").build(),"message".getBytes ());

客户端发出请求是生产者,客户端接收来自服务端的响应是消费者。

服务端接收来自客户端的请求是消费者,服务端将结果响应给客户端是生产者。

10、持久化

RabbitMQ的持久化分为三部分:

  • 交换器持久化:声明交换器时设置durable参数为true;
  • 队列持久化:声明队列时设置durable参数为true;
  • 消息持久化:生产者发布消息时设置deliveryMode为2;

由于交换器不存储消息,即使交换器不持久化,RabbitMQ重启后并不会丢失消息;

队列持久化只持久队列元数据,不会持久化消息,即使设置了队列持久化,RabbitMQ重启后仍然会丢失消息;

消息持久化可以真正持久化消息,但若不持久化队列,那么RabbitMQ重启后仍然会丢失消息;

如何确保数据不会丢失?

即使交换器、队列和消息都持久化,仍然会丢失消息,主要有以下几点考虑。

消费者autoAck如果设为true,那么当消费者收到消息还没来得及处理就挂掉了。解决办法:autoAck设为false;

由于有操作系统缓存,消息持久化并不会立马同步到磁盘,假如在同步磁盘前RabbitMQ挂掉了,则消息丢失。解决办法:使用RabbitMQ镜像队列机制,即使用从节点做数据备份,除非整个集群都挂掉,否则消息是不会丢失的;

发送端引入事务机制或确认机制,来确保消息是成功发送到了RabbitMQ,并且还要确保消息能够正确地理由到相应的队列中;

11、生产者确认机制

RabbitMQ为生产者提供了两种机制来确保消息成功发送到了服务端:

  • 事务机制
  • 确认机制

 生产者事务机制和确认机制只要确保消息发送到了交换器即可,不管这个交换器能不能正确地把消息路由到队列中。

事务机制和确认机制只能二选一,不能共存。

11.1、事务机制

相关方法:

  • channel.txSelect:将当前信道设成事务模式;
  • channel.txCommit:如果事务提交成功,则消息一定到达了RabbitMQ;
  • channel.txRollback:用try-catch块包裹住channel.txCommit,如果事务提交不成功,则可以进行回滚;
try {
	channel.txSelect();
	// 不存在该交换器
	channel.basicPublish("test_tx1","key",true,false,
			null,("hello,world:tx1").getBytes());
	// 存在该交换器
	channel.basicPublish("test_tx2","key",true,false,
			null,("hello,world:tx2").getBytes());
	// 消息会发送失败,然后回滚,原本能正确发送的消息hello,world:tx2也会回滚
	channel.txCommit();
} catch (IOException e) {
	e.printStackTrace();
	channel.txRollback();
}

11.2、确认机制

由于事务机制十分消耗性能,因此RabbitMQ引入了一种轻量级的替代方案:确认机制。

事务机制是同步的,发送一条消息之后立马阻塞,等RabbitMQ回应之后才会发送下一条消息。

确认机制是异步的,通过回调函数处理。

生产者通过channel.confirmSelect方法将信道设置为确认模式,RabbitMQ会在消息正确发送到交换器后给生产者返回一个确认。如果没有对应的交换器,则直接抛出异常。

try {
	channel.confirmSelect();
	// 没有该交换器,直接抛出异常
	channel.basicPublish("no_this_exchange", "key",null, "message".getBytes());
	if (!channel.waitForConfirms()) {
		System.out.println("消息发送失败");
	}else{
		System.out.println("消息发送成功");
	}

	// 有该交换器,但是没有绑定的队列,消息会发送成功
	channel.basicPublish("test_confirm", "key",null, "message".getBytes());
	if (!channel.waitForConfirms()) {
		System.out.println("消息发送失败");
	}else{
		System.out.println("消息发送成功");
	}
} catch (InterruptedException e){
	e.printStackTrace();
}

每发送一条消息,就调用一次waitForConfirms效率有点慢,可以有以下两种方案:

1、批量confirm:每发送一批消息确认一次,如果确认失败,则重新发送这批消息,代码如下所示。

channel.confirmSelect();
int count = 0;
List<String> list = new ArrayList<>();
for (;;) {
	channel.basicPublish("test_confirm", "key", null, "message".getBytes());
	// 将发送出去的消息存入缓存中
	list.add("message");
	if (++count >= 10) {
		count = 0;
		if (channel.waitForConfirms()) {
			System.out.println("批量确认成功!");
			// 将缓存中的消息清空
			list.clear();
		}else{
			System.out.println("批量确认失败,需要将缓存起来的消息重发!");
		}
	}
	// 每半秒钟发送一次消息
	Thread.sleep(500);
}

2、异步confirm,代码如下所示。

// 为每个channel维护一个unconfirm的消息序号集合
TreeSet<Long> unconfirmSet = new TreeSet();

channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener() {
	// deliveryTag是消息的唯一序列号
	@Override
	public void handleAck(long deliveryTag, boolean multiple) {
		System.out.println("异步确认,消息发生成功");
		if (multiple) {
			unconfirmSet.headSet(deliveryTag+1).clear();
		} else {
			unconfirmSet.remove(deliveryTag);
		}
	}

	@Override
	public void handleNack(long deliveryTag, boolean multiple) {
		System.out.println("异步确认,消息发生失败,消息需要重发!");
		// 在这里添加消息重发的逻辑
		if (multiple) {
			unconfirmSet.headSet(deliveryTag+1).clear();
		} else {
			unconfirmSet.remove(deliveryTag);
		}

	}
});

// 每半秒发生一条消息
for (;;) {
	// 当处于confirm模式时,返回下一条要发布的消息的序列号
	long nextSeqNo = channel.getNextPublishSeqNo();
	channel.basicPublish("test_confirm","key",null,"message".getBytes());
	// 每发布一条消息,unconfirmSet就添加一个元素
	unconfirmSet.add(nextSeqNo);
	Thread.sleep(500);
}

总结:事务机制和普通confirm机制编程简单,但吞吐量低,批量confirm和异步confirm则相反,根据实际场景选择不同的应对方式。

12、消息的顺序性和传输保障

RabbitMQ很难保证消息的顺序性,具体的原因有:

  • 多个生产者同时发消息,无法保证消息到达Broker的先后顺序;
  • 对于事务机制,假设RabbitMQ异常,那么消息发送失败,需要回滚消息,补偿消息时使用了另一个线程;
  • 对于确认机制,假设RabbitMQ异常,那么消息发送失败,补偿消息时使用了另一个线程;
  • 对消息设置了不同的TTL,某些消息进入了死信队列中;
  • 对消息设置了优先级;
  • 队列中消息是有序的,但有多个消费者消费同一队列,尽管轮询机制这也是有序的,但如果消费者拒绝了消息并requeue,那么消息就会错序了;

如果要保证消息的顺序性,需要业务方使用RabbitMQ之后做进一步的处理,比如在消息体内添加ID来实现。

消息的传输保证分三个层级:

最多一次:消息可能丢失,但绝不会重复传输,这个比较简单;

最少一次:消息不会丢失,但可能会重复传输。实现方案如下:

  • 生产者开启确认机制,保证消息可靠地传输到RabbitMQ中;
  • 生产者发布消息时将mandatory参数设为true,并且使用备份交换器;
  • 消息和队列都要持久化,防止RabbitMQ重启消息丢失;
  • 消费者autoAck设为false,并手动确认;

恰好一次:消息恰好消费一次;这个是无法保证的,具有情形如下:

  • 消费者在消费完一条消息之后向RabbitMQ发送确认命令,此时由于网络断开或者其他原因造成RabbitMQ并没有收到这个确认命令,那么RabbitMQ 不会将此条消息标记删除,在重新建立连接之后,消费者还是会消费到这一条消息,这就造成了重复消费;
  • 生产者在使用publisher confirm 机制的时候,发送完一条消息等待RabbitMQ 返回确认通知,此时网络断开,生产者捕获到异常情况,为了确保消息可靠性选择重新发送,这样RabbitMQ 中就有两条同样的消息,在消费的时候,消费者就会重复消费;

对于以上情形,RabbitMQ也没有去重机制来保证恰好一次,目前大多数的消息中间件也没有去重机制,一般是根据实际场景通过业务进行去重。

13、环境、配置与日志

13.1、环境

配置rabbitmq环境(优先级为从高到低)

  1. shell中设置,以RABBITMQ_开头
  2. rabbitmq-env.conf中设置,不要RABBITMQ_开头
  3. 内置

比如节点名称的环境变量为RABBITMQ_NODENAME,默认为rabbit@hostname,执行rabbitctl cluster_status命令如下图所示。

RabbitMQ入门教程_消息队列_05

下面演示如何修改环境变量RABBITMQ_NODE_PORT。

cat RabbitMQ安装目录/sin/rabbitmq-defaults,如下图所示。

RabbitMQ入门教程_AMQP_06

从该文件中可以看到环境变量配置文件的路径。

vi /etc/rabbitmq/rabbitmq-env.conf,添加环境变量,如下图所示。

NODE_PORT=5673

重启rabbitmq即可生效。

13.2、配置

环境变量RABBITMQ_CONFIG_FILE指定了配置文件路径,默认为/etc/rabbitmq/rabbitmq。

13.3、日志

RabbitMQ的日志默认在/var/log/rabbitmq目录下,如下图所示。

RabbitMQ入门教程_API_07

而RabbitMQ服务日志就是RABBITMQ_NODENAME.log。

14、使用消息队列的缺点

系统可用性降低:本来其他系统只要运行好好的,那你的系统就是正常的。现在加个消息队列进去,如果消息队列挂了,那系统就挂了。因此,系统可用性降低。
系统复杂性增加:要多考虑很多方面的问题,比如一致性问题、如何保证消息不被重复消费,如何保证消息可靠传输。因此,需要考虑的东西增多,系统复杂性增大。

15、ActiveMQ、RocketMQ、RabbitMQ、Kafka对比

特性

ActiveMQ

RocketMQ

RabbitMQ

kafka

开发语言

Java

Java

erlang

scala

单机吞吐量

万级

10万级

万级

10万级

时效性

ms级

ms级

us级

ms级

可用性

高(主从架构)

非常高(分布式架构)

高(主从架构)

非常高(分布式架构)

功能特性

成熟的产品,在很多公司得到应用;有较多的文档;各种协议支持较好

MQ功能比较完备,扩展性佳

基于erlang开发,所以并发能力很强,性能极其好,延时很低;管理界面较丰富

只支持主要的MQ功能,像一些消息查询,消息回溯等功能没有提供,毕竟是为大数据准备的,在大数据领域应用广。

                
如何选型?

  • 中小型软件公司,建议选RabbitMQ.一方面,erlang语言天生具备高并发的特性,而且他的管理界面用起来十分方便。正所谓,成也萧何,败也萧何!它的弊端也在这里,虽然RabbitMQ是开源的,然而国内有几个能定制化开发erlang的程序员呢?所幸,RabbitMQ的社区十分活跃,可以解决开发过程中遇到的bug,这点对于中小型公司来说十分重要。不考虑rocketmq和kafka的原因是,一方面中小型软件公司不如互联网公司,数据量没那么大,选消息中间件,应首选功能比较完备的,所以kafka排除。不考虑rocketmq的原因是,rocketmq是阿里出品,如果阿里放弃维护rocketmq,中小型公司一般抽不出人来进行rocketmq的定制化开发,因此不推荐。
  • 大型软件公司,根据具体使用在rocketMq和kafka之间二选一。一方面,大型软件公司,具备足够的资金搭建分布式环境,也具备足够大的数据量。针对rocketMQ,大型软件公司也可以抽出人手对rocketMQ进行定制化开发,毕竟国内有能力改JAVA源码的人,还是相当多的。至于kafka,根据业务场景选择,如果有日志采集功能,肯定是首选kafka了。具体该选哪个,看使用场景。