RabbitMQ系列P2—消息确认机制

193 阅读5分钟

  本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

前言

  一般情况下,消息由生产者生产后发送到Broker并存储到队列Queue中,再由消费者消费。但如果消息由生产端发送到Broker过程中出现意外情况,那么就会造成消息的丢失。那么我们如何保证消息成功发送到Broker端呢?

  这个时候消息确认机制就派上了用场。消息确认机制是指,生产者投递完消息之后,如果Broker收到消息,则会给生产者发送一条确认消息。生产者通过确认消息来判断消息是否成功发送到Broker,这种方式是消息投递可靠性的核心保障。

  RabbitMQ提供了两种确认消息是否投递成功的机制:

  • AMQP协议提供的事务机制;

  • RabbitMQ提供的confirm模式

  :两种模式不能同时使用,只能选择其中的一种

AMQP提供的事务机制

  事务的实现主要是对Channel进行设置,主要的方法有三个:

channel.txSelect() // 声明启动事务模式;

channel.txCommit() // 提交事务;

channel.txRollBack() // 回滚事务;

  通过txSelect()方法开启事务之后,如果txCommit()提交成功则表明消息已经成功达到了Broker之中,如果在txCommit执行之前Broker异常崩溃或者出现网络异常等等,这个时候就可以通过txRollBack()方法回滚事务,然后重发消息。但需要注意的是,事务会降低RabbitMQ的性能。下面是事务的演示代码。

在本地启动Rabbit,然后在pom.xml引入RabbitMQ的依赖

        <!-- RabbitMQ依赖 -->
<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.4.0</version>
</dependency>

  编写测试代码

public class TxProducer {
​
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
    
        // ConnectionUtil 抽离出连接本地Rabbit的逻辑用以复用
        Connection connection= ConnectionUtil.getConnection();
        
        Channel channel = connection.createChannel();
        
        String exchangeName="exchange_tx_confirm";
        
        String routingKey="tx.confirm";
        
        String message="test for tx mode";
​
​
​
        //开启事务模式
        channel.txSelect();
​
        //用于比较事务的性能
        long start=System.currentTimeMillis();
        
        try {
            channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
            channel.txCommit();
            System.out.println("消息已成功发送到Broker");
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("消息发送到Broker失败");
            channel.txRollback();
            // 重发 或者 入库
        }
​
​
       //测试性能
//        for (int i = 0; i <100 ; i++) {
//            try {
//                channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
//                channel.txCommit();
//                System.out.println("消息已成功发送到Broker");
//            } catch (IOException e) {
//                e.printStackTrace();
//                System.out.println("消息发送到Broker失败");
//                channel.txRollback();
//                // 重发 或者 入库
//            }
//        }
//
//        long end=System.currentTimeMillis();
//
//        System.out.println("事务耗时:"+(end-start)+" ms");
​
        channel.close();
        connection.close();
    }
}


public class Consumer {
​
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        String exChangeName="exchange_ordinary_confirm";
        String type="direct";
        String queueName="queue_ordinary_queue";
        String routingKey="ordinary.confirm";
​
        channel.exchangeDeclare(exChangeName, type, true);
        channel.queueDeclare(queueName, true, false, false, null);
        channel.queueBind(queueName, exChangeName, routingKey);
​
        MyConsumer consumer=new MyConsumer(channel);
        channel.basicConsume(queueName, true, consumer);
​
    }
}


public class MyConsumer extends DefaultConsumer {
​
    private Channel channel;
​
    public MyConsumer(Channel channel) {
        super(channel);
    }
​
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        System.out.println("consumerTag: \n"+consumerTag);
        System.out.println("envelope: \n"+envelope);
        System.out.println("properties: \n"+properties.getMessageId());
        System.out.println("message: \n"+new String(body));
    }
}



分别启动生产端和消费端,生产端控制台输出:

生产端已发送消息,message:test for tx mode 消费端控制台输出:

consumerTag: amq.ctag-Ig5FN1baG9OcRORQ0xhu0w
envelope: Envelope(deliveryTag=1, redeliver=false, exchange=exchange_tx_confirm, routingKey=tx.confirm)
properties: null
message: test for tx mode confirm机制

confirm机制

  使用事务机制来保证消息被成功从生产端发送到Broker中,但事务机制会降低RabbitMQ的性能。这时,我们就可以使用Rabbit提供的confirm模式。

  confirm模式的最大好处在于它是异步执行的。使用confirm模式时,生产端就可以在等待返回确认的同时继续发送下一条消息,提高效率。Broker成功收到消息之后会给生产端发送一条Ack消息;如果消息服务器出现内部错误等原因导致消息丢失,则会发送一条Nack消息。

  当生产端将channel设为confirm模式时,所有在该channel上发布的消息都会被指派一个唯一的id(从1开始),当消息被投递到匹配的队列后,Broker就会发送一个确认(包含消息的唯一id)给生产端,此时生产端就知道消息已成功发送到Broker端了。如果消息和队列是可持久化的,那么确认消息将在消息被写入磁盘之后发出,Broker回传给生产端的确认消息中的deliver-tag域包含了确认消息的序列号,此外Broker也可以设置basic.ackmultiple域,表示到这个序列号之前的所有消息都已经得到了处理

channel被设置成confirm模式后,所有被publish的后续消息都将被ack或者被nack一次,不会既被ack又被nack。需要注意的是,RabbitMQ并没有对confirm消息的快慢做保证。

  生产端的confirm模式有三种:

  • 普通confirm模式

  • 批量confirm模式

  • 异步confirm模式。

普通confirm模式

  普通confirm模式就是生产端发送一条消息就等待确认一条。使用方式如下:

channel.confirmSelect();
channel.basicPublish(exchange,routingKey,null,message.getByte());
channel.waitForConfirms();
// channel.waitForConfirms()返回true或者false,消息成功返回true;反之,则false

public class Producer {
​
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Connection connection= ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        String exchangeName="exchange_ordinary_confirm";
        String routingKey="ordinary.confirm";
        String message="test for ordinary confirm mode";
​
        long start=System.currentTimeMillis();
        //开启confirm模式
        channel.confirmSelect();
​
​
        channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
        if (channel.waitForConfirms()){
            System.out.println("消息已成功发送到Broker");
        }else {
            System.out.println("消息发送到Broker失败");
            //业务逻辑  重新发送 或者入库
        }
​
        //测试性能
//        for (int i = 0; i <100 ; i++) {
//            channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
//            if (channel.waitForConfirms()){
//                System.out.println("消息已成功发送到Broker");
//            }else {
//                System.out.println("消息发送到Broker失败");
//                //业务逻辑  重新发送 或者入库
//            }
//        }
//
//        long end=System.currentTimeMillis();//        System.out.println("普通confirm耗时:"+(end-start)+" ms");
​
        channel.close();
        connection.close();
    }
}

  启动生产端,控制台输出如下:

消息已成功发送到Broker

  运行生产端,控制台输出如下:

consumerTag: amq.ctag-e3Pp2h-Yr6NXbJQ2lQqVfQ
envelope: Envelope(deliveryTag=1, redeliver=false, exchange=exchange_ordinary_confirm, routingKey=ordinary.confirm)
properties: null
message: test for ordinary confirm mode

  消费端程序可参考之前的事务模式。普通confirm模式相比于事务来说,效率只高了一点点。这是因为普通confirm模式其实也是阻塞的,但比事务模式少了一步提交事务的步骤。事务是发送完消息之后还有提交事务,等待Broker返回事务提交成功的消息,而普通confirm模式发完消息后等待Brokerack消息。

批量模式

  所谓的批量模式,其实是指将多条消息发送完之后,再调用channel.waitForConfirms()方法来判断这一批消息是否成功发送到了Broker

public class BatchProducer {
​
    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Connection connection= ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        String exchangeName="exchange_batch_confirm";
        String routingKey="batch.confirm";
​
        long start=System.currentTimeMillis();
​
        //开启confirm模式
        channel.confirmSelect();
        for (int i = 0; i <100 ; i++) {
            String message="test for ordinary batch mode "+i;
            channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
        }
        if (channel.waitForConfirms()){
            System.out.println("消息已成功发送到Broker");
        }else {
            System.out.println("消息发送到Broker失败");
            //业务逻辑  重新发送 或者入库
        }
​
        long end=System.currentTimeMillis();
​
        System.out.println("批量confirm耗时:"+(end-start)+" ms");
    }
}

  批量confirm模式相比于普通confirm模式来说,效率提高了不少。但也有明显的缺点,若是waitForConfirms()方法返回false或者超时,那么这一批次的消息要全部重发,这样的话,效率并不比普通confirm模式要高。

异步confirm模式

  生产端可以通过调用channel.addConfirmListener(ConfirmListener listener)的方式来实现异步confirm模式。ConfirmListener提供了两个方法 handleAck(long deliveryTag,boolean multiple)handleNack(long deliveryTag,boolean multuple)方法分别处理队列Broker返回的Basic.AckBasic.Nack。其中参数deliveryTag就是我们上面提到过的标记消息的唯一id,而multiple则表示是否批量确认。

  对于异步confirm模式来说,我们需要为每一个channel维护一个unconfirm的消息序列集合,每当生产端发送一条消息时,集合中元素+1。若是Broker返回ack,则unconfir”集合则删除相应的一条或多条记录。如果multiplefalse,则从集合中删除当前deliveryTag元素,如果multipletrue,则将集合中小于等于当前序号deliveryTag元素的集合清除,表示这批序号的消息都已经被ack了。这个unconfirm集合最好使用有序集合SortedSet的存储结构来存储deliveryTag

public class AsynchronousProducer {
​
    static SortedSet<Long> unconfirm = Collections.synchronizedSortedSet(new TreeSet<>());
​
    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        String exchangeName = "exchange_confirm";
        String routingKey = "confirm.test";
​
​
        
​
        channel.confirmSelect();
​
        long start=System.currentTimeMillis();
        
        //addConfirmListener要在发布消息之前
        channel.addConfirmListener(new ConfirmListener() {
​
            @Override
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                //批量确认:将unconfirm集合中小于等于当前序号的deliveryTag清除掉,表示这些消息都已经被ack了
                if (multiple) {
                    System.out.println("ack批量确认,deliveryTag:" + deliveryTag + ",multiple:" + multiple + ",当次确认消息序号集合:" + unconfirm.headSet(deliveryTag + 1));
                    unconfirm.headSet(deliveryTag + 1).clear();
                } else {
                    //单条确认
                    System.out.println("ack单条确认,deliveryTag:" + deliveryTag + ",multiple:" + multiple + ",当次确认消息序号" + deliveryTag);
                    unconfirm.remove(deliveryTag);
                }
            }
​
            @Override
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                if (multiple) {
                    unconfirm.headSet(deliveryTag+1).clear();
                    // 重发 或者 入库
​
                } else {
                    unconfirm.remove(deliveryTag);
                    //重发 或者 入库
​
                }
            }
        });
​
        
​
        for (int i = 0; i <100 ; i++) {
            String message = "test asynchronous confirm " + i;
            long nextSeqNo = channel.getNextPublishSeqNo();
            channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
            System.out.println("消费端发送了消息,message:" + message);
            i++;
            unconfirm.add(nextSeqNo);
        }
        long end=System.currentTimeMillis();
​
        System.out.println("异步confirm耗时:"+(end-start)+" ms");
    }
}



  分别使用这四种方式发消息进行性能对比,每次生产100条消息,取三次结果的平均值,比较其耗时,分别如下:

事务耗时:38.3 ms
普通confirm耗时:37.3 ms
批量confirm耗时:11.3 ms
异步confirm耗时:8.0 ms

总结

  综上所述,我们可以使用AMQP协议的事务机制或者RabbitMQ提供的confirm模式来确保生产端的消息成功发送到Broker中。就效率而言,异步confirm>批量confirm>普通confirm>事务。