[RabbitMQ] RabbitMQ消息可靠性保证:消息确认/重传/持久化/死信队列等

1 消息确认

1.1 消息发送确认

当一个消息发送出去之后,我们需要知道它是否发送成功。在RabbitMQ中提供了:

1)Confirm callback:确认消息是否到达broker。

2) Return callback :当消息到达broker后,exchange 路由消息的时候,如果根据 routing key 无法找到一个匹配的队列,则触发此回调(在发送消息时候需要指定 mandatory = true)

import com.rabbitmq.client.*;
import java.io.IOException;

public class MqClient {
    private static int tryNum = 1000;
    public static void topicQueueTest() throws Exception{
        String EXCHANGE_NAME = "topic_exchange";
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("192.168.1.91");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.confirmSelect();
        channel.addConfirmListener(new RbmqConfirmListener());//Confirm events 消息发送到broker后触发回调
        channel.addReturnListener(new RbmqReturnListener());//无法找到一个匹配的队列的时候触发
        channel.addShutdownListener(new RbmqShutdownListener());
        channel.exchangeDeclare(EXCHANGE_NAME, "topic", true);
        AMQP.BasicProperties props = setSendPros();
        channel.basicPublish(EXCHANGE_NAME,"quick.orange.rabbit", true,props, "topic:quick.orange.rabbit".getBytes());
        channel.basicPublish(EXCHANGE_NAME,"lazy.orange.elephant", true,props, "topic:lazy.orange.elephant".getBytes());
        channel.basicPublish(EXCHANGE_NAME, "lazy.brown.fox", true, props, "topic:lazy.brown.fox".getBytes());
        channel.basicPublish(EXCHANGE_NAME, "a.b.rabbit", true, props, "topic:a.b.rabbit".getBytes());
        while( tryNum-- > 0) { Thread.sleep(1000); }
        channel.close();
        connection.close();
    }
    private static AMQP.BasicProperties setSendPros() {
       return MessageProperties.PERSISTENT_TEXT_PLAIN;
    }
    public static void main(String[] args) throws Exception {
        //routeQueueTest();
        //fanoutQueueTest();
        topicQueueTest();
    }
}

class RbmqConfirmListener implements  ConfirmListener {
   //消息正确到达broker
    @Override
    public void handleAck(long deliveryTag, boolean multiple) throws IOException {
        System.out.println(System.currentTimeMillis() + " RbmqConfirmListener::handleAck messageID:" + deliveryTag + " multiple:" + multiple);
    }
   //消息丢失
    @Override
    public void handleNack(long deliveryTag, boolean multiple) throws IOException {
        System.out.println(System.currentTimeMillis() + " RbmqConfirmListener::handleNack messageID:" + deliveryTag + " multiple:" + multiple);
    }
}

class RbmqReturnListener implements  ReturnListener {
//无法找到一个匹配的队列时调用
    @Override
    public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
        System.out.println(System.currentTimeMillis() + " RbmqReturnListener::handleReturn replyCode:" + replyCode + " replyText:" + replyText +
                " exchange:" + exchange + " routingKey:" + routingKey);
    }
}

///

import com.rabbitmq.client.*;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class MqServer {
    

    public static void topicConsumer() throws Exception {
        final  String EXCHANGE_NAME = "topic_exchange";
        final String QUEUE_NAME = "topic_queue_1";
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost("10.129.2.91");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE_NAME, true, false, false,null);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "*.orange.*");
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "lazy.#");
        channel.basicQos(1);//告诉服务器,在我们没有确认当前消息完成之前,不要给我们发生消息
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
                System.out.println("消费者1收到内容是:" + new String(body));
                channel.basicAck(envelope.getDeliveryTag(), false);//参数2,为了减少网络流量,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
            }
        };
        //注册消费者,参数2.代表我们收到消息后需要手动告诉服务器:我收到消息了
        channel.basicConsume(QUEUE_NAME, false, consumer);
        new BufferedReader(new InputStreamReader(System.in)).readLine();
        channel.close();
        connection.close();
    }

    public static void main(String[] argv) throws Exception {
        
        topicConsumer();
    }
}

运行(只启动发送者且确保队列不存在):

1550823065618 RbmqReturnListener::handleReturn replyCode:312 replyText:NO_ROUTE exchange:topic_exchange routingKey:quick.orange.rabbit
1550823065618 RbmqConfirmListener::handleAck messageID:1 multiple:false
1550823065618 RbmqReturnListener::handleReturn replyCode:312 replyText:NO_ROUTE exchange:topic_exchange routingKey:lazy.orange.elephant
1550823065618 RbmqConfirmListener::handleAck messageID:2 multiple:false
1550823065618 RbmqReturnListener::handleReturn replyCode:312 replyText:NO_ROUTE exchange:topic_exchange routingKey:lazy.brown.fox
1550823065618 RbmqConfirmListener::handleAck messageID:3 multiple:false
1550823065618 RbmqReturnListener::handleReturn replyCode:312 replyText:NO_ROUTE exchange:topic_exchange routingKey:a.b.rabbit
1550823065618 RbmqConfirmListener::handleAck messageID:4 multiple:false

---------------------

先启动消费者,再运行生产者:

1550823349393 RbmqReturnListener::handleReturn replyCode:312 replyText:NO_ROUTE exchange:topic_exchange routingKey:a.b.rabbit
1550823349409 RbmqConfirmListener::handleAck messageID:4 multiple:false
1550823349409 RbmqConfirmListener::handleAck messageID:2 multiple:true
1550823349409 RbmqConfirmListener::handleAck messageID:3 multiple:false

消费者1收到内容是:topic:quick.orange.rabbit
消费者1收到内容是:topic:lazy.orange.elephant
消费者1收到内容是:topic:lazy.brown.fox

1.2 消息接收确认

消息通过ACK 确认是否被消费者正确接收。每个 Message 都要被确认(acknowledged),可以手动去 ACK 或自动 ACK

自动ACK(默认): 在消息发送给消费者后立即确认并从队列中删除消息。如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息。

手动确认:消费者调用ack,nack,reject几种方法进行确认。手动确认可以在业务失败后进行一些操作,如果消息未被 ACK 且连接断开后则会将消息发生给其他消费者。

在上面的例子中:

DefaultConsumer consumer = new DefaultConsumer(channel) {
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("消费者1收到内容是:" + new String(body));
        channel.basicReject(envelope.getDeliveryTag(), true);//拒绝消息,这时服务器会重发消息    //channel.basicNack(envelope.getDeliveryTag(), false, true);//批量拒绝消息
        //channel.basicAck(envelope.getDeliveryTag(), false);//消息消费成功,服务器把消息从队列中删除
//参数2,为了减少网络流量,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息
    }
};
//在注册消费者,参数2.代表我们收到消息后需要手动ACK
channel.basicConsume(QUEUE_NAME, false, consumer);

运行:

1550825188423 消费者1收到内容是:topic:quick.orange.rabbit
1550825189608 消费者1收到内容是:topic:quick.orange.rabbit
1550825190794 消费者1收到内容是:topic:quick.orange.rabbit
1550825191979 消费者1收到内容是:topic:quick.orange.rabbit

消费者会一直收到相同的重传消息。

(此时只有一个消费者监听该队列,则有发生死循环的风险,多消费端也会造成资源的极大浪费,这个在开发过程中一定要避免的)。

Overview Messages Message rates +/-
Name Features State Ready Unacked Total incoming deliver / get ack
fanout_queue_1 D idle 0 0 0      
route_queue_1 D idle 2 0 2      
topic_queue_1 D running 2 1 3 0.00/s 1.0/s 0.00/s

 

1.3消息重传

对于RabbitMQ服务器端而言,队列中的消息分成了两部分:

一部分是等待投递给消费者的消息;

一部分是已经投递给消费者,但是还没有收到消费者ack信号的消息。

如果服务器端一直没有收到消费者的ack信号,并且消费此消息的消费者已经断开连接,则服务器端会安排该消息重新进入队列,等待投递给下一个消费者。

 消费者通过ACK 机制中的nack(批量拒绝)/reject (enqueue参数),让服务器是否发起重传。
    

2 持久化

为了保证RabbitMQ在退出,服务重启或者crash等异常情况下,也不会丢失消息,我们可以将Queue,Exchange,Message都设置为可持久化的(durable),这样可以保证绝大部分情况下我们的RabbitMQ消息不会丢失。当然还是会有一些小概率事件会导致消息丢失。

  • exchange要持久化: 如果exchange不设置持久化,那么当broker服务重启之后,exchange将不复存在,那么既而发送方rabbitmq producer就无法正常发送消息
  • channel.exchangeDeclare(EXCHANGE_NAME, "topic", true);
  • queue要持久化: 
    channel.queueDeclare(QUEUE_NAME, true, false, false,null);
    队列是可以被持久化,但是里面的消息是否为持久化那还要看消息的持久化设置。
  • message要持久化: MessageProperties.PERSISTENT_TEXT_PLAIN 。channel.basicPublish("exchange.persistent", "persistent", MessageProperties.PERSISTENT_TEXT_PLAIN, "persistent_test_message".getBytes())

3 死信队列

当消息在一个队列中变成死信(dead message)之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX( Dead-Letter-Exchange)。消息变成死信有以下几种情况:

    消息被拒绝(basic.reject 或basic.nack)并且requeue=false
    消息TTL过期(Time-To-Live 过期时间)
    队列达到最大长度

DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定.实际上就是设置某个队列的属性,当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列,可以监听这个队列中消息做相应的处理.

channel.exchangeDeclare("dlx.exchange.name", "direct");
Map args = new HashMap<>();
args.put("x-max-length", 10240);
args.put("x-dead-letter-exchange", "dlx.exchange.name");
channel.queueDeclare(QUEUE_NAME, true, false, false,args);

 

你可能感兴趣的:(RabbitMQ)