RabbitMQ生产者发送消息确认

在使用RabbitMQ的时候,可以通过消息的持久化操作来解决因为服务器的异常崩溃而导致的消息丢失,除此之外,我们还会遇到一个问题,当消息的生产者将消息发送出去以后,消息到底有没有到达服务器呢?如果不进行特殊的配置,默认情况下发送消息的操作是不会返回任何消息给生产者的,也就是默认情况下是不知道消息有没有正确地到达服务器。如果在消息到达服务器之前已经丢失,持久化操作也解决不了这个问题,因为消息根本没有到达服务器,何谈持久化?

RabbitMQ针对这个问题,提供了两种解决方法:

  ❤ 事务机制

  ❤ 发送方确认机制

事务机制

RabbitMQ客户端中与事务机制相关的方法有三个:channel.txSelect、channel.txCommit、channel.txRollback。channel.txSelect用于将当前的信道设置成事务模式,channel.txCommit用于提交事务,channel.txRollback用于事务回滚。

在通过channel.txSelect方法开启事务之后,我们便可以发布消息给RabbitMQ了,如果事务提交成功,则消息一定到达了RabbitMQ中,如果在事务提交之前由于RabbitMQ异常崩溃或者其他的原因抛出异常,这个时候我们可以将其捕获,进而通过执行channel.txRollback方法来实现事务回滚。

部分示例代码如下:

     channel.txSelect();
        channel.basicPublish("exchange","routingkey",MessageProperties.PERSISTENT_TEXT_PLAIN,"hello".getBytes());
        channel.txCommit();

上述代码是正常的情况下的事务机制的运转过程,而事务回滚是什么样子的呢?下面的代码示例:

  try {
            channel.txSelect();
            channel.basicPublish("exchange","routingkey",MessageProperties.PERSISTENT_TEXT_PLAIN,"hello".getBytes());
            int result = 1 / 0;
            channel.txCommit();
        }catch (Exception e){
            e.printStackTrace();
            channel.txRollback();
        }

 上述代码中明显的有一个java.lang.ArithmeticException,在事务提交之前捕获异常,之后显示的回滚事务。

如果要发送多条消息,则将channel.basicPublish和channel.txCommit等方法包裹进循环内即可,可以参考以下代码:

  channel.txSelect();
        for (int i = 0;i < 1000;i++){
            try {

                channel.basicPublish("exchange","routingkey",MessageProperties.PERSISTENT_TEXT_PLAIN,"hello".getBytes());
                int result = 1 / 0;
                channel.txCommit();
            }catch (Exception e){
                e.printStackTrace();
                channel.txRollback();
            }
        }

事务确实能够解决消息发送方和RabbitMQ之间消息确认的问题,只有消息成功被RabbitMQ接收,事务才能提交成功,否则便可在捕获异常之后进行事务回滚,于此同时可以进行消息重发。但是使用事务机制会“吸干”RabbitMQ的性能,那么有没有更好的方法既能保证确认消息已经正确送达,又能基本上不带来性能上的损失呢?从AMQP协议层面上看来没有更好的方法,但是RabbitMQ提供了一个改进方案,即发送方确认机制。

发送方确认机制

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

事务机制在发送一条消息之后就会使得发送端阻塞,以等待RabbitMQ的回应,之后才能继续发送下一条消息。相比之下,发送方确认机制最大的好处就是在于它是异步的,一旦发布一条消息,生产者应用程序就可以在等待信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用程序便可以通过回调方法来处理确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack(Basic.Nack)命令,生产者应用程序同样可以在回调方法中处理该nack命令。

生产者通过调用channel.confirmSelect方法(即Confirm.Select命令)将信道设置为confirm模式,之后RabbitMQ会返回Confirm.Select-Ok命令表示同意生产者将当前信道设置为confirm模式,所有被发送的后续消息都被ack或者nack一次,不会出现一条消息既被ack又被nack的情况,并且RabbitMQ也没有对消息的被confirm的快慢做出任何保证。

通过下面的例子来看一下publisher confirm机制怎么运作,代码如下:

  try {
            channel.confirmSelect();
            channel.basicPublish("exchange","routingkey",MessageProperties.PERSISTENT_TEXT_PLAIN,"hello".getBytes());
            if (!channel.waitForConfirms()){
                System.out.println("message failed!");
                // do something
            }
        }catch (Exception e){
            e.printStackTrace();
        }

 如果发送多条消息,只需要将channel.basicPublish和channel.waitForConfirms方法包裹在循环里面即可,可以参考事务机制,不过不需要把channel.confirmSelect方法包裹在循环内部。

对于channel.waitForConfirms而言,在RabbitMQ客户端有它的4个同类的方法:

  (1)boolean waitForConfirms() throws InterruptedException;

  (2)boolean waitForConfirms(long timeout) throws InterruptedException;

  (3)void waitForConfirmsOrDie() throws IOException,InterruptedException;

  (4)void waitForConfirmsOrDie(long timeout) throws IOException,InterruptedException, TimeoutException;

如果信道没有开启publisher confirm模式,那么调用任何的waitForConfirms方法都会报错java.lang.IllegalStateException。对于没有参数的waitForConfirms方法来说,其返回的条件是客户端收到了相应的Basic.Ack/.Nack或者被中断。参数timeout表示超时时间,一旦等待RabbitMQ回应超时就会抛出java.util.concurrent.TimeoutException异常。两个waitForConfirmsOrDie方法在接收到RabbitMQ返回的Basic.Nack之后抛出java.io.IOException。业务代码可以根据自身的特性灵活的运用这四种方法来保障消息的可靠发送。

注意:

  ❤ 事务机制和publisher confirm机制是两者互斥的,不能共存。如果企图将已开启事务模式的信道再设置为publisher confirm模式,RabbitMQ会报错。或者企图将已开启publisher confirm模式的信道再设置为事务模式,RabbitMQ也会报错;

  ❤ 事务机制和publisher confirm机制确保的是消息能够正确的发送至RabbitMQ,这里的“发送至RabbitMQ”的含义是指消息被正确的发送至RabbitMQ的交换器,如果此交换器没有匹配的队列,那么消息也会丢失。所以在使用这两种机制的时候要确保所涉及的交换器能够有匹配的队列。更进一步的讲,发送方要配合mandatory参数或者备份交换器一起使用来提高消息传输的可靠性。

publisher confirm的优势在于并不一定需要同步确认。可以改进一下使用方式:

  (1)批量confirm方法:每发送一批消息后,调用chann.waitForConfirms方法,等待服务器的确认返回

  (2)异步confirm方法:提供一个回调方法,服务端确认了一条或者多条消息后客户端会回调这个方法进行处理

批量confirm方法

在批量confirm方法中,客户端程序需要定期或者定量(达到多少条),亦或者两者结合起来调用chann.waitForConfirms来等待RabbitMQ的确认返回。但是存在一个问题就是在返回Basic.Nack或者超时的情况下,客户端需要将这一批的消息全部重发,这会带来明显的重复消息数量,并且当消息经常丢失时,批量confirm的性能是不升反降的。

批量confirm代码如下:

  try {
            channel.confirmSelect();
            int nsgCount = 0;
            while (true){
                channel.basicPublish("exchange","routingkey",MessageProperties.PERSISTENT_TEXT_PLAIN,"hello".getBytes());
                //将发送出去的消息存入缓存中,缓存可以是一个ArrayList或者BlockQueue之类的
                if (++nsgCount >= 1000){
                    nsgCount = 0;
                    try {
                        if (channel.waitForConfirms()){
                            //将缓存中消息清空
                            continue;
                        }
                        //将缓存中消息重发
                    }catch (InterruptedException e){
                        e.printStackTrace();
                        //将缓存中的消息重发
                    }
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }

异步confirm方法

异步confirm方法的编程实现是最复杂的,在客户端的channel接口中提供的addConfirmListener方法可以添加ConfirmListener这个回调接口,这个ConfirmListener接口包含两个方法:handleAck和handleNack,分别用来处理RabbitMQ回传的Basic.Ack和Basic.Nack。在这两个方法中都包含有一个参数deliveryTag(在publisher confirm模式下用来标记消息的唯一有序序号)。我们需要为每一个信道维护一个“unconfirm”的消息序号集合,每发送一条消息,集合中的元素就加1.每当调用ConfirmListener中的handleAck方法时,“unconfirm”集合中就删除相应的一条(multiple设置为false)或者多条(multiple设置为true)记录。从程序的运行效率来看,这个“unconfirm”集合最好采用有序集合SortedSet的存储结构。事实上,Java端SDK中的waitForConfirms方法也是通过SortedSet维护消息序号的。

下面的代码示例:

     SortedSet confirmSet = new TreeSet();
        channel.confirmSelect();
        channel.addConfirmListener(new ConfirmListener() {
            @Override
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("Nack,SeqNo: " + deliveryTag + ", multiple: " + multiple);
                if (multiple){
                    confirmSet.headSet(deliveryTag + 1);
                }else {
                    confirmSet.remove(deliveryTag);
                }
            }

            @Override
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                if (multiple){
                    confirmSet.headSet(deliveryTag + 1).clear();
                }else {
                    confirmSet.remove(deliveryTag);
                }
                //这里添加处理消息重发的场景
            }
        });

        //这里模仿一直发送消息的场景
        while (true){
            long nextSeqNo = channel.getNextPublishSeqNo();
            channel.basicPublish("exchange","routingkey",MessageProperties.PERSISTENT_TEXT_PLAIN,"hello".getBytes());
            confirmSet.add(nextSeqNo);
        }

 将事务、普通confirm、批量confirm、和异步confirm一起来比较它们的QPS,如下图所示:

可以看出批量和异步这两种方式所呈现的性能要比其余两种好的多。不过异步和批量的编程比较复杂,普通和事务编程较简单。

不过还是推荐使用批量和异步来实现。

参考:《RabbitMQ实战指南》 朱忠华 编著; 

posted on 2019-05-24 08:19  AoTuDeMan  阅读(2550)  评论(0编辑  收藏  举报

导航