RabbitMQ高可靠性传输解决方案

    在RabbitMQ使用过程当中存在一些问题。比如发送消息我们如何确保消息的投递的可靠性呢?如何保证消费消息可靠性呢?

生产者可靠性投递

  • 在使用RabbitMQ的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景.RabbitMQ为我们提供了两种方式用来控制消息的投递可靠性模式,RabbitMQ提供了如下两种模式:
    1. confirm模式: 生产者发送消息到交换机的时机
    2. return模式: 交换机转发消息给queue的时机

RabbitMQ投递消息的流程如下:
在这里插入图片描述

  1. 生产者发送消息到交换机
  2. 交换机根据routingKey转发消息给队列
  3. 消费者监控队列,获取队列中的消息
  4. 消费成功后,删除队列中的消息
  • 消息从product发送到exchange则会返回一个confirmCallback
  • 消息从exchange发送到queue则会返回一个returnCallback

ConfirmCallback

配置文件application.yml

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    publisher-confirm-type: correlated
server:
  port: 18081

重点关注publisher-confirm-type: correlated,publisher-confirm-type发布确认属性配置.一共有三种确认类型,如下:

	/**
	 * The type of publisher confirms to use.
	 */
	public enum ConfirmType {
    
    

		/**
		 * Use {@code RabbitTemplate#waitForConfirms()} (or {@code waitForConfirmsOrDie()}
		 * within scoped operations.
		 */
		SIMPLE,

		/**
		 * Use with {@code CorrelationData} to correlate confirmations with sent
		 * messsages.
		 */
		CORRELATED,

		/**
		 * Publisher confirms are disabled (default).
		 */
		NONE

	}
  • NONE:禁用发布确认模式,是默认值;
  • CORRELATED:发布消息成功到交换器后会触发回调方法;
  • SIMPLE:经测试有两种效果
    1. 效果和CORRELATED值一样会触发回调方法;
    2. 在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker;

创建队列;交换机和绑定

@Configuration
public class RabbitMQConfig {
    
    

    /**
     * 创建队列
     * @return
     */
    @Bean
    public Queue createqueue(){
    
    
        return new Queue("queue_demo01");
    }

    /**
     * 创建交换机
     * @return
     */
    @Bean
    public DirectExchange createExchange(){
    
    
        return new DirectExchange("exchange_direct_demo01");
    }

    /**
     * 创建绑定
     * @return
     */
    @Bean
    public Binding createBinding(){
    
    
        return BindingBuilder.bind(createqueue()).to(createExchange()).with("demo");
    }
}

创建confirm回调函数

@Component
public class RabbitMQConfirm implements RabbitTemplate.ConfirmCallback {
    
    


    /**
     * confirm模式回调函数
     *
     * @param correlationData 消息信息
     * @param ack             确认标记;true:exchange收到消息;false没有收到消息
     * @param cause           没有收到消息的原因;如果收到消息输出null
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
    
    
        if (ack){
    
    
            System.out.println("消息发送交换机成功;原因:"+cause);
            return;
        }
        System.out.println("消息发送交换机失败;原因:"+cause);
    }
}

创建controller发送消息

@RestController
public class DemoController {
    
    

    @Autowired
    RabbitMQConfirm rabbitMQConfirm;

    @Autowired
    RabbitTemplate rabbitTemplate;

    @GetMapping(value = "/send/{exchange}/{routingKey}/{msg}")
    public void send(@PathVariable(value = "exchange") String exchange,
                     @PathVariable(value = "routingKey") String routingKey,
                     @PathVariable(value = "msg") String msg) {
    
    
        // 设置confirm回调函数
        rabbitTemplate.setConfirmCallback(rabbitMQConfirm);
        rabbitTemplate.convertAndSend(exchange, routingKey, msg);
    }
}

测试结果

正确测试: http://localhost:18081/send/exchange_direct_demo01/demo/一条消息
在这里插入图片描述
错误测试(传一个不存在的交换机): http://localhost:18081/send/exchange_direct_demo/demo/一条消息
在这里插入图片描述

总结

  1. 生产者可以根据confirm机制来确保消息是否已经发送到交换机
  2. confirm机制只能保证消息发送到交换机有回调,不能保证消息转发到queue有回调

ReturnCallback

修改配置文件application.yml

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    publisher-confirm-type: correlated
    publisher-returns: true
server:
  port: 18081

publisher-returns属性为true时,开启ReturnCallback机制;默认为false关闭.

创建return回调函数

@Component
public class RabbitMQReturns implements RabbitTemplate.ReturnCallback {
    
    

    /**
     * return模式回调函数
     *
     * @param message    消息信息
     * @param replyCode  退回的状态码
     * @param replyText  退回的信息
     * @param exchange   交换机
     * @param routingKey 路由键
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
    
    
        System.out.println("消息:"+message);
        System.out.println("退回的状态码:"+replyCode);
        System.out.println("退回的原因:"+replyText);
        System.out.println("退回的交换机:"+exchange);
        System.out.println("退回的routingKey:"+routingKey);
    }
}

修改controller发送消息

@RestController
public class DemoController {
    
    

    @Autowired
    RabbitMQConfirm rabbitMQConfirm;

    @Autowired
    RabbitMQReturns rabbitMQReturns;

    @Autowired
    RabbitTemplate rabbitTemplate;

    @GetMapping(value = "/send/{exchange}/{routingKey}/{msg}")
    public void send(@PathVariable(value = "exchange") String exchange,
                     @PathVariable(value = "routingKey") String routingKey,
                     @PathVariable(value = "msg") String msg) {
    
    
        // 设置confirm回调函数
        rabbitTemplate.setConfirmCallback(rabbitMQConfirm);
        // 设置return回调函数
        rabbitTemplate.setReturnCallback(rabbitMQReturns);
        rabbitTemplate.convertAndSend(exchange, routingKey, msg);
    }
}

测试结果

正确测试: http://localhost:18081/send/exchange_direct_demo01/demo/一条消息
在这里插入图片描述
错误测试(传一个不存在的routingKey): http://localhost:18081/send/exchange_direct_demo01/demo1/一条消息
在这里插入图片描述

总结

  1. return模式只有再消息从交换机发送到队列出现错误时使用
  2. 一般情况下不会使用return模式,因为routingKey是由开发人员来指定,一般不会出现错误

消费者确认机制(ACK)

生产者保证可靠性投递,但是在消费者也有可能出现问题,比如没有接受消息,比如接受到消息之后,在代码执行过程中出现了异常,这种情况下我们需要额外的处理,那么就需要手动进行确认签收消息.RabbitMQ给我们提供了一个机制:ACK机制.

配置文件application.yml

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    listener:
      simple:
        acknowledge-mode: manual
server:
  port: 8081

acknowledge-mode: 签收消息的方式.一共有三种方式,如下:

public enum AcknowledgeMode {
    
    

	/**
	 * No acks - {@code autoAck=true} in {@code Channel.basicConsume()}.
	 */
	NONE,

	/**
	 * Manual acks - user must ack/nack via a channel aware listener.
	 */
	MANUAL,

	/**
	 * Auto - the container will issue the ack/nack based on whether
	 * the listener returns normally, or throws an exception.
	 * <p><em>Do not confuse with RabbitMQ {@code autoAck} which is
	 * represented by {@link #NONE} here</em>.
	 */
	AUTO;

	/**
	 * Return if transactions are allowed - if the mode is {@link #AUTO} or
	 * {@link #MANUAL}.
	 * @return true if transactions are allowed.
	 */
	public boolean isTransactionAllowed() {
    
    
		return this == AUTO || this == MANUAL;
	}

	/**
	 * Return if the mode is {@link #NONE} (which is called {@code autoAck}
	 * in RabbitMQ).
	 * @return true if the mode is {@link #NONE}.
	 */
	public boolean isAutoAck() {
    
    
		return this == NONE;
	}

	/**
	 * Return true if the mode is {@link #MANUAL}.
	 * @return true if manual.
	 */
	public boolean isManual() {
    
    
		return this == MANUAL;
	}

}
  1. NONE: 自动确认;当消息一旦被消费者接收到,则自动确认收到,并将相应消息从RabbitMQ的消息缓存中移除.但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失.
  2. MANUAL: 手动确认,需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()等方法,让其按照业务功能进行处理,比如:重新发送,比如拒绝签收进入死信队列等等.
  3. AUTO: 根据异常情况来确认(暂时不怎么用)

消费者代码

未签收

@Component
public class Listener {
    
    

    @RabbitListener(queues = "queue_demo01")
    public void msg(Message message, Channel channel , String msg) {
    
    
        System.out.println("队列queue_demo01收到消息:"+msg);
    }
}

测试

在这里插入图片描述
在这里插入图片描述
消息虽然成功消费了,但是一直没有被签收,等到项目重新启动还会再次进行消费.使用手动签收模式时,一定要记得签收消息.

手动签收

@Component
public class Listener {
    
    

    @RabbitListener(queues = "queue_demo01")
    public void msg(Message message, Channel channel, String msg) {
    
    
        try {
    
    
            System.out.println("队列queue_demo01收到消息:" + msg);
            System.out.println("业务代码执行中....");
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
    
    
            System.out.println("消息消费失败");
        }
    }
}

测试

在这里插入图片描述
在这里插入图片描述
消息已经被成功签收,但是业务代码出现异常时,还是没有做处理

丢弃消息

@Component
public class Listener {
    
    

    @RabbitListener(queues = "queue_demo01")
    public void msg(Message message, Channel channel, String msg) {
    
    
        try {
    
    
            System.out.println("队列queue_demo01收到消息:" + msg);
            System.out.println("业务代码执行中....");
            int i = 1/0;
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
    
    
            System.out.println("消息消费失败");
            try {
    
    
                // 丢弃消息
                channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
            } catch (IOException ioException) {
    
    
                ioException.printStackTrace();
            }
        }
    }
}

测试

在这里插入图片描述

重回消息队列

@Component
public class Listener {
    
    

    @RabbitListener(queues = "queue_demo01")
    public void msg(Message message, Channel channel, String msg) {
    
    
        try {
    
    
            System.out.println("队列queue_demo01收到消息:" + msg);
            System.out.println("业务代码执行中....");
            int i = 1/0;
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
    
    
            System.out.println("消息消费失败");
            try {
    
    
                // 丢弃消息
                //channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
                // 重回消息队列
                channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);
            } catch (IOException ioException) {
    
    
                ioException.printStackTrace();
            }
        }
    }
}

测试

在这里插入图片描述
重回消息队列就相当于重新发送一次消息,如果是我们的代码有问题,那么这条消息就会一直进行重发

总结

业务代码出现异常时,可以将消息进行丢弃,也可以重回消息队列,也可以拒签进入死信队列等等,具体怎么做根据业务场景来.

写到最后

RabbitMQ如何保证消息高可靠性传输?

  1. 持久化,exchange要持久化,queue要持久化,message要持久化
  2. 生产者确认Confirm,Return
  3. 消费者手动签收消息,以及消费者代码出现异常的处理(根据业务场景来)

Guess you like

Origin blog.csdn.net/qq_43135259/article/details/120953680