com.rabbitmq:amqp-client

目录

安装与配置

AMQP协议介绍

客户端开发(连接、交换机、队列、生产/消费消息)

基础概念

交换机类型

RabbitMQ运转流程

消费消息:推模式和拉模式

消费端消息确认与拒绝

队列类型(死信、延迟、优先级队列)

死信队列

 延迟队列

 优先级队列

持久化与生产者确认

消息分发、顺序、传输保障

消息分发

消息顺序性

消息传输保障

队列结构、流量控制

存储机制

队列结构

流量控制


安装配置

  1. Downloading and Installing RabbitMQ :https://www.rabbitmq.com/download.html
  2. RabbitMQ中文文档:RabbitMQ中文文档 · RabbitMQ in Chinese
  3. 教程:https://www.rabbitmq.com/getstarted.html
  4. 常用指令
启动 rabbitmqctl -server -detached
查看是否启动成功 rabbitmqctl status
查看集群信息 rabbitmqctl cluster_status
创建用户 rabbitmqctl add_user username psd
设置用户所有权限 rabbitmqctl set_permissions -p/username ". *" ". *" ". *"
设置用户权限 rabbitmqctl [--node ] [--longnames] [--quiet] set_permissions [--vhost ]
命令帮助 rabbitmqctl help

AMQP协议介绍

        消息队列的运转过程如下图所示,首先生产者将业务方数据进行可能的包装,之后封装 成消息,发送(AMQP协议里这个动作对应的命令为Basic.Publish)到Broker中。消费者订阅并接收消息(AMQP协议里这个动作对应的命令为 Basic.Consume或者Basic.Get),经过可能的解包处理得到原始的数据,之后再进行业务处理逻辑。这个业务处理逻辑并不一定需要和接收消息的逻辑使用同一个线程。 消费者进程可以使用一个线程去接收消息, 存入到 内存中,比如使用Java中的BlockingQueue。业务处理逻辑使用另一个线程从内存中读取数据,这样可以将应用进一步解耦,提高整个应用的处理效率。

com.rabbitmq:amqp-client_第1张图片

        AMQP协议本身包括三层:

  1. Module Layer:位于协议最高层,主要定义了一些共客户端调用的命令,客户端可以利用这些命令实现自己的业务逻辑。如:Queue.Declare命令用于声明队列。
  2. Session Layer:位于中间层,主要负责将客户端命令发送给服务器,再将服务器端的应答返回给客户端,主要为客户端与服务器之间的通信提供可靠性同步机制和错误处理。
  3. Transport Layer:位于最底层,主要传输二进制数据流,提供帧的处理、信道复用、错误检测和数据表示等。
AMQP生产者流转过程  AMQP 消费者流转过程(推模式)
com.rabbitmq:amqp-client_第2张图片 com.rabbitmq:amqp-client_第3张图片

 更多信息可以在AMQP官网下下载,RabbitMQ使用的是0-9-1版。

客户端开发(连接、交换机、队列、生产/消费消息)

基础概念

  1. 生产者/消费者:消息的发送方和消费方。消息一般包括消息体和标签两部分。消息体一般是带有业务逻辑的数据,标签一般用来描述消息。消费时,只能消费消息的消息体。消息在路由过程中,消息标签会丢弃,只有消息体才能存入队列中。
  2. 队列:用于存储消息,当多个消费者订阅统一队列时,默认采用平均分摊(轮询)规则处理消息。
  3. 交换机:RabbitMQ中,生产者将消息发送到交换机,然后再通过路由键(Routing Key)将消息路由到与绑定建(Binding Key)匹配的队列中。点对点模式时,可以直接发送给队列。
  4. 路由键:生产者将消息发送给交换机时,一般会指定一个RoutingKey,用来指定这个消息的路由规则。
  5. 绑定键:通过绑定键把交换机与队列关联起来。结合生产者指定的RoutingKey,才可以正确的将消息路由到队列。

        生产者将消息发送给交换器时, 需要一个 RoutingKey,当BindingKey和RoutingKey相 匹配时,消息会被路由到对应的队列中。 在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的BindingKey。BindingKey并不是在所有的情况下都生效,它依赖于交换器 类型,比如fanout类型的交换器就会无视 BindingKey,而是将消息路由到所有绑定到该交换器的队列中。

交换机类型

  1. fanout:将消息路由到所有绑定到该交换器的队列中
  2. direct:把消息路由到BindingKey和RoutingKey完全匹配的队列中
  3. topic规则:
  • RoutingKey为一个点号“.”分隔的字符串(被点号“.”分隔开的每一段独立的字符串称为一个单词), 如“com. rabbitmq. client”、“java. util. concurrent”、“com. hidden. client”;
  •  BindingKey和RoutingKey一样也是点号“.”分隔的字符串;
  • BindingKey中可以存在两种特殊字符串“*”和“#”,用于做模糊匹配,其中“*”用于匹配一个单词,“#”用于匹配多规格单词(可以是零个)。
  • 举例:

com.rabbitmq:amqp-client_第4张图片

(1) 路由键为“com. rabbitmq. client”的消息会同时路由到Queue1和Queue2;

(2) 路由键为“com. hidden. client”的消息只会路由到Queue2 中;

(3) 路由键为“com. hidden. demo”的消息只会路由到Queue2 中;

(4) 路由键为“java. rabbitmq. demo”的消息只会路由到Queue1中;

(5) 路由键为“java. util. concurrent”的消息将会被丢弃或者返回给生产者(需要设置mandatory参数),因为它没有匹配任何路由键。

RabbitMQ运转流程

  • 生产者生产消息过程:

(1) 生产者连接到RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)。

ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(HOST);
connectionFactory.setPort(PORT);
/**
 * 如果配置有用户名密码以及vhost,则配置即可。
 */
connectionFactory.setUsername(USERNAME);
connectionFactory.setPassword(PASSWORD);
connectionFactory.setVirtualHost(VIRTUALHOST);
//使用uri方式
//connectionFactory.setUri(“qmqp://userName:password@ipAddress:protNumber/virtualHost”);
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();

Connection可以用来创建多个Channel实例,但是Channel实例不能在线程间共享,多线程共享Channel实例是线程不安全的,应用程序应该为每个线程开辟一个channel。

慎用Connection或者Channel的isOpen方法检测是否处于开启状态。

(2) 生产者声明一个交换器,并设置相关属性,比如交换机类型是否持久化等。

channel.exchangeDeclare("exchange.confirm", "direct", true, false, null);
  • 函数Exchange.DeclareOk exchangeDeclare参数介绍,有多个重载方法:

        (1) String exchange:交换机名称

        (2) String type:交换机类型(fanout/direct/topic)

        (3) boolean durable:是否持久化;持久化后服务器重启不丢失相关信息

        (4) boolean autoDelete:是否自动删除,自动删除的前提是至少有一个队列或者交换器与这个交换器绑定,之后所有与这个交换器绑定的队列或者交换器都与此解绑。

        (5) boolean Internal:设置是否是内置的。如果设置为true,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器这种方式。

        (6) Map arguments:其他结构化参数

  • void exchangeDeclareNoWait方法,多了一个nowait参数,不建议使用。
  • Exchange.DeclareOk exchangeDeclarePassive(String name)方法,主要用来检测交换机是否存在,存在正常返回,不存在抛出异常。
  • 其他方法可以在Channel接口中查看。

(3) 生产者声明一个队列并设置相关属性,比如是否排他、是否持久化、是否自动删除等。

channel.queueDeclare("queue.basicqos", true, false, false, null);
  • Queue. DeclareOk queueDeclare有两个重载方法,一个无惨、一个有参,参数介绍:

        (1) String queue:队列名称

        (2) boolean durable:是否设置持久化

        (3) boolean exclusive:设置是否排他。为true则设置队列为排他的。如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。这里 需要注意三点:

                (a)排他队列是基于连接(Connection)可见的,同一个连接的不同信道(Channel)是 可以同时访问同一连接创建的排他队列;

                (b) “首次”是指如果一个连接已经声明了一个排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同;

                (c)即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除,这种队列适用于一个客户端同时发送和读取消息的应用场景。

        (4) boolean autoDelete:设置是否自动删除。为true则设置队列为自动删除。自动除的前提是:至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除。

        (5) Map arguments:其他结构化参数

  • 其他方法可以在Channel接口中查看。
  • 生产者和消费者都能够使用queueDeclare来声明一个队列, 但是如果消费者在同一个信道上订阅了另一个队列, 就无法再声明队列了。必须先取消订阅,然后将信道置为“传输”模式,之后才能声明队列。
    • 如果需要情况队列内容,可以调用Queue. PurgeOk queuePurge(String queue) throws IOException;

(4) 生产者通过路由键将交换器和队列绑定起来。

channel.queueBind("queue.basicqos", "exchange.confirm", "routingKey2");
  • 队列与交换机绑定

        (1) String queue:队列名称

        (2) String exchange:交换机名称

        (3) String routingKey:路由键

        (4) Map arguments:其他结构化参数

  • 交换机与交换机绑定Exchange. BindOk exchangeBind

        (1)String destination:

        (2) String source:

        (3)String routingKey:路由键

        (4)Map arguments:其他结构化参数

  •  解绑等其他方法在Channel接口中查看。

(5) 生产者发送消息至RabbitMQ Broker,其中包含路由键、交换器等信息。

// 消息发布,这个地方的ROUTING_KEY是routing key,当binding key和routing key匹配时,队列才会收到
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY,MessageProperties.PERSISTENT_TEXT_PLAIN,
        message.getBytes());

两个关键属性:mandatory和immediate

  • mandatory:当设置为true时,如果交换机无法通过自身类型以及路由键找到队列时,会调用Basic.Return将详细返回给生产者。当为false时,出现上述情况,直接丢弃。
  • immediate:当设置为true时,如果发现队列没有绑定消费者时,会会调用Basic.Return将详细返回给生产者。RabbitMQ 3.0以后版本不再支持。

生产者需要通过Channel.addReturnListener()添加ReturnListener监听器,来处理返回的消息。当然可以设置备份交换机(Alternate Exchange),当消息无法路由时,通过备份交换机存储,而不返回客户端。

(6) 相应的交换器根据接收到的路由键查找相匹配的队列。

  • 如果找到,则将从生产者发送过来的消息存入相应的队列中。
  • 如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者。

(7) 关闭信道和连接

if (channel != null) {
    channel.close();
}
if (connection != null) {
    connection.close();
}
  • 备份交换机:可以通过在声明交换器(调用channel.exchangeDeclare方法)时添加alternate- exchange参数来实现,也可以通过策略的方式实现。如果两者同时使用,则前者的优先级更高,会覆盖掉Policy的设置。

com.rabbitmq:amqp-client_第5张图片

 对于备份交换器,总结了以下几种特殊情况:

  • 客户端和RabbitMQ服务端都不会有异常出现且消息会丢失的情况:
    1. 如果设置的备份交换器不存在
    2. 如果备份交换器没有绑定任何队列
    3. 如果备份交换器没有任何匹配的队列
  • 如果备份交换器和mandatory参数一起使用,那么mandatory参数无效。
  • 消费者接收消息过程:

(1)消费者连接到RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)。

(2)消费者向RabbitMQ Broker请求消费相应队列中的消息,可能会设置相应的回调函数, 以及做一些准备工作。

(3)等待RabbitMQ Broker回应并投递相应队列中的消息,消费者接收消息。

(4)消费者确认(ack)接收到的消息。

(5)RabbitMQ从队列中删除相应已经被确认的消息。

(6)关闭信道。

(7)关闭连接。

Address[] addresses = new Address[]{new Address(IP_ADDRESS, PORT)};
ConnectionFactory factory = new ConnectionFactory();
//factory.setVirtualHost("/vhost_xyn");
factory.setUsername("root");
factory.setPassword("123456");
Connection connection = factory.newConnection(addresses);
final Channel channel = connection.createChannel();
 // 设置客户端最多未被ack的个数
channel.basicQos(Constant.NUM64);
Consumer consumer = new DefaultConsumer(channel) {
        @Override
        public void handleDelivery( String consumerTag,Envelope envelope,
                                    AMQP.BasicProperties properties,
                                    byte[] body ) throws IOException {
            log.info(" recv message: " + new String(body));
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                log.info("catch InterruptedException");
            }
            /**
             * deliveryTag(唯一标识 ID):当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,
             * RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 delivery tag,
             * 它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,
             * delivery tag 的范围仅限于 Channel
             * multiple:为了减少网络流量,手动确认可以被批处理,当该参数为 true 时,
             * 则可以一次性确认 delivery_tag 小于等于传入值的所有消息
             */
            channel.basicAck(envelope.getDeliveryTag(), false);
        }
    };
    /**
     * callback:设置消费者的回调函数。用来处理RabbitMQ推送过来的消息
     * 比如DefaultConsumer,使用时需要重写其中的方法
     */
channel.basicConsume(QUEUE_NAME, consumer);
    // 等待回调函数执行完毕之后 关闭资源
 TimeUnit.SECONDS.sleep(Constant.NUM5);
 ConnectionUtils.close(channel, connection);

消费消息:推模式和拉模式

  • 使用Basic.Consume的推模式

(1)通过持续订阅的方式获取消息,使用到的相关类有:com.rabbitmq.client.Consumer、com.rabbitmq.client.DefaultConsumer;接收消息一般通过实现Consumer接口或继承DefaultConsumer类来实现。当调用与 Consumer相关的API方法时,不同的订阅采用不同的消费者标签(consumerTag)来区分彼此,在同一个Channel 中的消费者也需要通过唯一的消费者标签以作区分。代码片段如下:

channel.basicQos(NUM5);
Consumer consumer = new DefaultConsumer(channel) {
    @Override
    public void handleDelivery( String consumerTag, Envelope envelope,
                AMQP.BasicProperties properties, byte[] body ) throws IOException {
        log.info(" recv message: " + new String(body));
        // 控制是接收到的第几个消息
        num++;
        // 模拟业务处理耗时1秒
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            log.info("catch InterruptedException");
        }
        // 只让是5的倍数的消息发ack消息
        if (num % NUM5 == 0) {
            channel.basicAck(envelope.getDeliveryTag(), false);
        } else {
            log.trace("模拟不发ack情况");
            // channel.basicAck(envelope.getDeliveryTag(), false);
        }
    }
};
channel.basicConsume(QUEUE_NAME, consumer);

(2)Channel.basicConsume方法参数,说明:

String queue:队列名称

boolean autoAck:是否自动确认,建议设置为false

String consumerTag:消费者标签,用来区分多个消费者

boolean noLocal:设置为true表示不能将同一个Connection中生产者发送的消息发送给这个Connection中的消费者

boolean exclusive:是否排他

Map arguments:其他参数

Consumer callback:设置消费者回调函数,用来处理推送过来的消息

(3)当一个Channel中维持多个消费者时,如果Channel中的一个消费者一直在运行,那么其他消费者的callback会被“耽搁”。

(4)消费者客户端的这些callback会被分配到与Channel不同的线程池上,这意味着消费者客户端可以安全地调用阻塞方法, 比如channel. queueDeclare、channel. basicCancel 等。

  • 使用Basic.Get的拉模式

(1)通过channel.basicGet方法可以单条地获取消息,其返回值是GetRespone。Channel类的basicGet方法没有其他重载方法,只有:GetResponse basicGet(String queue, boolean autoAck) throws IOException;

(2)拉模式运转如图所示。com.rabbitmq:amqp-client_第6张图片

(3)Basic.Consume将信道置为投递模式,直到取消队列的订阅为止。 在投递模式期间,RabbitMQ会不断 地推送消息给消费者,推送消息的 个数受到Basic.Qos的限制。如果只想从队列获得单条消息而不是持续 订阅,建议使用Basic.Get进行消费。不能将Basic.Get放在一个循环里来代替Basic.Consume,这样严重影响RabbitMQ的性能。要实现高吞吐量,消费者使用 Basic. Consume方法。

消费端消息确认与拒绝

  • 确认:

为了保证消息从队列可靠地达到消费者,RabbitMQ提供了消息确认机制(message acknowledgement)。消费者在订阅队列时,可以指定autoAck参数,当autoAck等于false时,RabbitMQ会等待消费者显式地回复确认信号后才从内存(或者磁盘)中移去消息(实质上是先打上删除标记,之后再删除)。当autoAck等于true时,RabbitMQ会自动把发送出去的消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正地消费到了 这些消息。

RabbitMQ不会为未确认的消息设置过期时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否已经断开,这么设计的原因是RabbitMQ允许 消费者消费一条消息的时间可以很久。

  • 拒绝:

(1)Channel类中的basicReject方法定义如下:

  • void basicReject(long deliveryTag, boolean requeue) throws IOException; 其中,
  • deliveryTag可以看作消息的编号,它是一个64位的长整型值,最大值是 9223372036854775807。
  • 如果requeue参数设置为true,则RabbitMQ会重新将这条消息存入队列,以便可以发送给下一个订阅的消费者;
  • 如果requeue参数设置为false,则RabbitMQ 立即会把消息从队列中移除,而不会把它发送给新的消费者。

(2)Basic.Reject命令一次只能拒绝一条消息,如果想要批量拒绝消息,则可以使用 Basic. Nack这个命令。void basicNack(long deliveryTag, boolean multiple, boolean requeue) throwsIOException;

  • multiple 参数设置为true则表示拒绝deliveryTag编号之前所有未被当前消费者确认的消息。
  • 其他参数与basicReject含义一致。

(3)将channel.basicReject或者channel.basicNack中的requeue设置为false,可以启用“死信队列”的功能。死信队列可以通过检测被拒绝或者未送达的消息来追踪问题。

(4)Basic.RecoverOk basicRecover(boolean requeue):这个方法用来请求RabbitMQ重新 发送还未被确认的消息。如果requeue参数设置为true,则未被确认的消息会被重新加入到 队列中,这样对于同一条消息来说,可能会被分配给与之前不同的消费者。如果requeue参数设置为false,那么同一条消息会被分配给与之前相同的消费者。requeue默认为 true。

队列类型(死信、延迟、优先级队列)

死信队列

DLX,全称为Dead-Letter-Exchange,可以称之为死信交换器,也有人称之为 死信邮箱。当消息在一个队列中变成死信(dead message)之后,它能被重新被发送到另一个交换器中,这个交换器就是DLX,绑定DLX的队列就称之为死信队列。消息变成死信一般 是由于以下几种情况:

消息被拒绝(Basic.Reject/ Basic.Nack),并且设置 requeue参数为false;

消息过期

队列达到最大长度。

Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
// 声明死信交换器和正常交换器
channel.exchangeDeclare("exchange.dlx", "direct", true, false, null);
channel.exchangeDeclare("exchange.normal", "fanout", true, false, null);

// 正常队列的声明和绑定
Map arg = new HashMap();
arg.put("x-message-ttl", NUM10000);
arg.put("x-dead-letter-exchange", "exchange.dlx");// 设置死信队列
channel.queueDeclare("queue.normal", false, false, false, arg);
channel.queueBind("queue.normal", "exchange.normal", "");
// 死信队列的声明和绑定
channel.queueDeclare("queue.dlx", false, false, false, null);
channel.queueBind("queue.dlx", "exchange.dlx", "rk");

// 发送消息
String message = "delay message !";
channel.basicPublish("exchange.normal", "rk", false,
        MessageProperties.PERSISTENT_TEXT_PLAIN,
        message.getBytes());
ConnectionUtils.close(channel, connection);

com.rabbitmq:amqp-client_第7张图片

 延迟队列

AMQP和RabbitMQ均不直接支持延迟队列,但是可以通过DLX和TTL模拟。

  • 过期时间(TTL)设置方式:

(1)通过队列属性设置,所有消息有相同的过期时间

// 创建一个持久化、非排他的、非自动删除的队列
Map arg = new HashMap();
// 设置队列过期时间,单位:毫秒
arg.put("x-expires", NUM20000);
channel.queueDeclare(QUEUE_NAME, true, false, false, arg);

(2)对消息本身进行单独设置,每条消息的TTL不同

/ 发送一条持久化的消息: hello world !
String message = "Hello World !";

AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
builder.deliveryMode(NUM2);
builder.expiration("6000"); // 设置消息的TTL=6000ms
AMQP.BasicProperties properties = builder.build();
// properties中包括了expiration属性
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, false,
        properties, message.getBytes());

(3)消息是否过期是在即将投递到消费者之前判断的,所以通过队列设置TTL,消息一旦过期,就会从队列抹去;但是通过消息设置TTL,不会被马上抹去。

(4)延迟队列设计

com.rabbitmq:amqp-client_第8张图片

 优先级队列

具有高优先级的队列具有高的优先权,优先级高的消息具备优先被消费的 特权。

(1)设置队列优先级

Map arg = new HashMap();
arg.put("x-max-priority", NUM10);
channel.queueDeclare("queue_priority", true, false, false, arg);

(2)设置消息优先级

// 发送带有优先级的消息
for (int i = 0; i < NUM10; i++) {
    AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
    if (i % NUM2 != 0) {
        builder.priority(NUM5);
    }
    AMQP.BasicProperties properties = builder.build();
    channel.basicPublish("exchange_priority", "rk_priority", properties, ("messages-" + i).getBytes());
}

(3)只有当生产者速度大于消费者速度的时候,消息设置优先级才有意义。

持久化与生产者确认

  • 持久化:交换机持久化、队列持久化、消息持久化

(1)交换机不持久化,RabbitMQ服务重启后,交换机元数据丢失,消息不会丢失。

(2)队列不持久化,RabbitMQ服务重启后,队列元数据和消息都会丢失。

(3)消息持久化 + 队列持久化,才能保证RabbitMQ服务重启后,消息不丢失。

  • 保证宕机/重启时,除了持久化外,保证消息不丢失还需如下操作:

(1)配置镜像队列

(2)消费消息时,设置autoAck=false,并进行手动确认。

(3)生产消息时,通过生产者确认:通过事务机制实现或者通过发送方确认机制实现【推荐】,事务机制和publisher confirm机制互斥们不能共存。事务机制和publisher confirm机制 确保的是消息能够正确地发送至RabbitMQ,这里的“发送至RabbitMQ”的含义是指消息被正确地发往至RabbitMQ的交换器,如果交换器没有匹配的队列,那么消息也会丢失。所以在使用这两种机制的时候要确保所涉及的交换器能够有匹配的队列。更进一步讲,发送方要配合mandatory参数或者备份交换器一起使用来提高消息传输的可靠性。

(4)确认

生产者将信道设置成confirm(确认)模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),使得生产者知晓消息已经正确到达了目的地。如果消息和队列是持久化的,那么确认消息会在 消息写入磁盘之后发出。RabbitMQ回传给生产者的确认消息中的deliveryTag包含了确认消息的序号,此外RabbitMQ也可以设置channel.basicAck方法中的multiple参数,表示到这个 序号之前的所有消息都已经得到了处理。

com.rabbitmq:amqp-client_第9张图片

(4-1)普通确认

// 启动confirm模式
channel.confirmSelect();

String msg = "我是普通confirm模式";
// 发送单条消息
channel.basicPublish("exchange.confirm", "routingKey", MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
try {
    // 在此阻塞,等待回复,如果回复true,进入此分支
    if (channel.waitForConfirms()) {
        // 客户端收到了Basic.Ack
        log.info("发送成功");
    } else {
        // 客户端收到了Basic.Nack
        log.info("发送失败");
        log.info("重发消息逻辑");
    }
} catch (InterruptedException e) {
    // 被中断,抛出InterruptedException
    log.info("catch InterruptedException");
}

(4-2) 批量确认

channel.confirmSelect();// 启动confirm模式
// 批量发送消息
for (int i = 0; i < NUM5; i++) {
    String msg = String.format("时间 => %s", new Date().getTime());
    channel.basicPublish("exchange.confirm", "routingKey", MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
}
        channel.waitForConfirmsOrDie();// 直到所有信息都发布,只要有一个未确认就会IOException
        log.info("全部发送成功");// 客户端收到了Basic.Ack

(4-3)异步确认

// 启动confirm模式
channel.confirmSelect();

// 开启监听
channel.addConfirmListener(new ConfirmListener() {
    // 处理成功
    public void handleAck( long deliveryTag, boolean multiple ) throws IOException {
        log.info("消息发送成功: " + deliveryTag
                + ", multiple : " + multiple);
        // 收到消息,从消息序号队列中删除deliveryTag
        if (multiple) {
            confirmSet.headSet(deliveryTag - 1).clear();
        } else {
            confirmSet.remove(deliveryTag);
        }
    }

    // 处理失败
    public void handleNack( long deliveryTag, boolean multiple ) throws IOException {
        if (multiple) {
            confirmSet.headSet(deliveryTag - 1).clear();
        } else {
            confirmSet.remove(deliveryTag);
        }
    }
});

// 循环发送消息
for (int i = 1; i < NUM64; i++) {
    String msg = "我是confirm模式消息.异步[" + i + "]";
    long tag = channel.getNextPublishSeqNo();
    // 发送消息,将deliveryTag加入到消息序号队列
    channel.basicPublish("exchange.confirm", "routingKey2", MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
    log.info("tag:" + tag);
    confirmSet.add(tag);
}

消息分发、顺序、传输保障

消息分发

  1. RabbitMQ采用轮询分发机制,默认情况下,如果有n个消费者,RabbtitMQ会把第m条消息,分发给第m%n个消费者,RabbitMQ不管消费者是否消费并已经确认(Basic. Ack)了消息。【影响性能】
  2. channel.basicQos(int prefetchCount)可以设置消费者持有的最大未确认消息数量。
  3. channel.basicQos有三个重载方法:
  • channel.basicQos(int prefetchCount) throws IOException;
  • channel.basicQos(int prefetchCount, boolean global) throws IOException;
  • channel.basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException;
  • 参数说明:

(1)prefetchCount:预取个数, = 0 时,无上限。

(2)prefetchSize:消费者所能接收未确认消息的总体大小的上限,单位B,= 0时,无上限。

(3)global:对于一个信道来说,它可以同时消费多个队列,当设置了prefetchCount大于0时,这个信道需要和各个队列协调以确保发送的消息都没有超过所限定的 prefetchCount的值,这样会使RabbitMQ的性能降低,尤其是这些队列分散在集群 中的多个Broker节点之中。RabbitMQ为了提升相关的性能,在AMQP 0-9-1协议之上重新定义了global 这个参数。

 代码示例1:同一信道多个消费者,每个消费者各自未确认消息的上限均为10

Channel channel = ...;
Consumer consumer1 = ...;
Consumer consumer2 = ...;
channel. basicQos( 10);// Per consumer limit 
channel. basicConsume(" my- queue1", false, consumer1); 
channel. basicConsume(" my- queue2", false, consumer2);

代码示例2:同一信道既设置了global = true,又设置了global = false。那么每个消费者最多能收到3个未确认的消息,但是两个消费者收到的未确认消息的和上限为5。不建议如此设置global,最好使用默认值global = false。

Channel channel = ...;
Consumer consumer1 = ...; 
Consumer consumer2 = ...;
channel. basicQos( 3, false); // Per consumer limit 
channel. basicQos( 5, true); // Per channel limit 
channel. basicConsume(" queue1", false, consumer1);
channel. basicConsume(" queue2", false, consumer2);

消息顺序性

  1. 在只有一个消费者、一个生产者、没有消息丢失、不使用高级特性的时候,可以确保消费者消费消息的顺序跟生产者生产消息顺序的一致。
  2. 事务、生产者确认、消息优先级、多生产者、网络/服务器故障等都可能影响顺序。

消息传输保障

  • 中间件消息传输保障层级:
  1. At most once:最多一次。消息可能会丢失,但绝不会重复传输。
  2. At least once:最少一次。消息绝不会丢失,但可能会重复传输
  3. Exactly once:恰好一次。每条消息肯定会被传输一次且仅传输一次。
  • RabbitMQ支持:At most once和At least once,其中At least once投递实现需要考虑如下方面:
  1. 消息生产者需要开启事务机制或者publisher confirm机制,以确保消息可以可靠地传输到RabbitMQ中。
  2. 消息生产者需要配合使用mandatory参数或者备份交换器来确保消息能够从交换器 路由到队列中,进而能够保存下来而不会被丢弃。
  3. 消息和队列都需要进行持久化处理,以确保RabbitMQ服务器在遇到异常情况时不会 造成消息丢失。
  4. 消费者在消费消息的同时需要将autoAck设置为false,然后通过手动确认的方式去 确认已经正确消费的消息,以避免在消费端引起不必要的消息丢失。
  5. 消费时,要注意幂等性问题。

队列结构、流量控制

存储机制

  • windows系统默认情况下数据存在:C:\Users\%USERNAME%\AppData\Roaming
  • 持久化:非持久化信息和持久化信息都可以被写入磁盘
  1. 持久化的消息在到达队列时就被写入到磁盘,并且如果可以,持久化的消息也会在内存中保存一份备份,这样可以提高一定的性能,当内存吃紧的时候会从内存中清除。
  2. 非持久化的消息一般只保存在内存中,在内存吃紧的时候会被换入到磁盘中,以节省 内存空间。
  3. 持久层包括部分:队列索引(rabbit_queue_index)和消息存储(rabbit_msg_store)。
  • rabbit_queue_index负责维护队列中落盘消息的信息,包括消息的存储地点、是否 已被交付给消费者、是否已被消费者ack等。每个队列都有与之对应的一个rabbit_queue_index。
  • rabbit_msg_store以键值对的形式存储消息,它被所有队列共享,在每个节点中有 且只有一个。rabbit_msg_store具体还可以分为msg_store_persistent和msg_store_transient。
    1. msg_store_persistent:负责持久化消息的持久化,重启后消息不会丢失;
    2. msg_store_transient:负责非持久化消息的持久化,重启后消息丢失。

  4.消息(包括消息体、属性和headers)可以直接存储在rabbit_queue_index中,也可以被保存在rabbit_msg_store中。在默认路径下包含queues、msg_store_persistent、msg_store_transient这3个文件夹,其分别存储对应的信息。

   5.最佳的配备是较小的消息存储在rabbit_queue_index中而较大的消息存储在rabbit_msg_store中。

队列结构

  • 队列由rabbit_amqqueue_process和backing_queue这两部分组成,rabbit_amqqueue_process负责协议相关的消息处理,即接收生产者发布的消息、向消费者交付消息、处理消息的确认(包括生产端的confirm和消费端的ack)等。backing_queue是消息存储的具体形式和引擎,并向rabbit_amqqueue_process提供相关 的接口以供调用。
  • 如果消息投递的目的队列是空的,并且有消费者订阅了这个队列, 那么该消息会直接 发送给消费者,不会经过队列这一步。
  • RabbitMQ中的队列消息的状态:
  1. alpha:消息内容(包括消息体、属性和headers)和消息索引都存储在内存中。
  2. beta:消息内容保存在磁盘中,消息索引保存在内存中。
  3. gamma:消息内容保存在磁盘中,消息索引在磁盘和内存中都有。【持久化消息独有】
  4. delta:消息内容和索引都在磁盘中。
  • RabbitMQ在运行时会根据统计的消息传送速度定期计算一个当前内存中能够保存 的最大消息数量(target_ram_count),如果alpha状态的消息数量大于此值时,就会引起消息的状态转换。

状态

耗用分析

alpha

最耗内存,很少消耗CPU

beta

 只需要一次I/O操作

gamma

delta

基本不消耗内存,消耗很多CPU和磁盘I/O操作;需要执行两次I/O操作才能读取到消息,一次是读取消息索引、一次是读取消息内容。

  •  没有设置优先级和镜像的队列,backing queue的默认实现是rabbit_variable_queue,其内部通过5个子队列Q1、Q2、Delta、Q3、Q4来体现消息的各个状态。队列结构图如图所示。状态。整个队列包括 rabbit_amqqueue_process和 backing_queue的各个子队列。

com.rabbitmq:amqp-client_第10张图片

 

  • Q1、Q4只包含alpha状态的消息;
  • Delta只包含delta状态的消息;
  • Q2、Q3包含beta和gamma状态的消息

消费者获取消息时会从Q4开始。Q4 不为空,获取成功返回;Q4为空,从Q3获取;如果Q3为空,则队列为空,直接返回;

Q3不为空,取出数据,然后判断Q3、Delta的长度,如果为空,则Q2、Delta、Q3、Q4均为空, 将Q1中消息直接转移到Q4,下次直接从Q4中获取消息。如果Q3为空,Delta不为空,则将Delta消息转移到Q3,下次直接从Q3 获取消息。在将消息从Delta转移到Q3的过程中,是按照索引分段读取 的, 首先读取某一段,然后判断读取的消息的个数与Delta中消息的个数是否相等, 如果相等,则可以判定此时Delta中已无消息,则直接将Q2和刚读取到的消 息 一并放入到Q3中;如果不相等,仅将此次读取到的消息转移到Q3。

通常在负载正常时,如果消息被消费的速度不小于接收新消息的速度,对于不需要保证 可靠不丢失的消息来说,极有可能只会处于alpha状态。对于durable属性设置为true的消息,它一定会进入gamma状态,并且在开启publisher confirm机制时,只有到了 gamma状态时才会确认该消息已被接收,若消息消费速度足够快、内存也充足,这些消息也不会继续走到下一个状态。

惰性队列:RabbitMQ从3.6.0版本开始引入了惰性队列(Lazy Queue)的概念。惰性队列会尽可能地将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中, 它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。

流量控制

RabbitMQ 可以对内存和磁盘使用量设置阈值,当达到阈值后,生产者将被阻塞(block), 直到对应项恢复正常。除了这两个阈值,从2.8.0版本开始,RabbitMQ还引入了流控(Flow Control)机制来确保稳定性。流控机制是用来避免消息的发送速率过快而导致服务器难以 支撑的情形。内存和磁盘告警相当于全局的流控(Global Flow Control),一旦触发会阻塞集群中所有的Connection,而本节的流控是针对单个Connection的,可以称之为Per- Connection Flow Control或者Internal Flow Control。

你可能感兴趣的:(#,Spring,AMQP,批处理,java,rabbitmq)