RabbitMQ可靠性投递

RabbitMQ可靠性投递

  • 解释
  • 解决
    • 前提
    • 工作模式
    • 各个环节处理
      • 1、消息发送到服务器
        • 1、Transaction(事务)模式
        • 2、Confirm(确认)模式
          • ①、普通确认模式
          • ②、批量确认模式
          • ③异步确认模式
      • 2、消息从交换机路由到队列
        • 1)、消息回发的方式
        • 2)、消息路由到备份交换机的方式
      • 3、消息在队列存储
        • 1)、队列持久化
        • 2)、交换机持久化
        • 3)、消息持久化
        • 4)、集群
      • 4、消息投递到消费者
      • 5、消费者回调
        • 1)、调用生产者API
        • 2)、发送响应消息给生产者
      • 6、补偿机制
      • 7、消息幂等性
      • 8、消息的顺序性

解释

使用MQ的时候一定是先操作数据库再发送消息,这样是为了避免数据库回滚导致数据不一致。但是如果先操作数据后发送消息,一旦消息出了问题,一样会出现数据的不一致。此篇文章处理MQ可靠性投递的问题。

解决

前提

  • 在解决可靠性投递前要明确一个问题,因为效率和可靠性是无法兼得的,如果要保证每一个环节都成功,则会对消息的收发效率造成影响。如果是一些业务的一致性要求不是特别高的场合,其实是可以牺牲一些可靠性来换取效率的。比如:发送通知或者记录日志的场景。

工作模式

RabbitMQ可靠性投递_第1张图片

在使用RabbitMQ收发信息的时候,有以下几个环节需要注意:

  • ①代表消息从生产者发送到Broker 。 生产者如何确认自己的消息有没有被Broker成功接收。
  • ②代表消息从Exchange路由到Queue。 如果消息没有办法路由到正确的队列,怎么办。
  • ③代表消息在Queue中存储。 消息在没有消费者来消费时,队列出现了问题,消息丢失。该如何保证消息在队列中稳定的存储。
  • ④代表消费者订阅Queue并消费消息。 Broker如何知道消费者已经接收到了消息。

各个环节处理

1、消息发送到服务器

    第一个环节生产者如何确定Broker有没有正确的接收到消息。
在RabbitMQ里面提供两种截止服务端确认机制,也就是生产者发送消息给RabbitMQ服务端的时候,服务端会通过某种方式返回一个应答(ACK),只要生产者收到了这个应答(ACK),就知道消息发送成功了。

1、Transaction(事务)模式

RabbitMQ可靠性投递_第2张图片
    我们可以通过一个channel.txSelect();的方法吧信道设置成事务模式,然后就可以发布消息给RabitMQ了,如果channel.txCommit();方法调用成功,则说明事务提交成功,这个消息一定到达了RabbitMQ中。
    如果在事务提交之前由于RabbitMQ异常崩溃活其他原因抛出异常,这个时候我们便可以捕获异常,并执行channel.txRollback(); 方法来实现事务回滚。
    注意:在事务模式里面,只有收到了服务端的Commit-OK的指令,才能提交成功。所以可以解决生产者和服务端确认的问题。但是事务模式有一个缺点,它是阻塞的,一条消息没有发送完毕,不能发送下一条消息,它会榨干RabbitMQ服务器的性能。
示例:

	public static void main(String[] args) throws Exception {
	        ConnectionFactory factory = new ConnectionFactory();
	        factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));
	
	        // 建立连接
	        Connection conn = factory.newConnection();
	        // 创建消息通道
	        Channel channel = conn.createChannel();
	
	        String msg = "Hello world, Rabbit MQ";
	        // 声明队列(默认交换机AMQP default,Direct)
	        // String queue, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
	        channel.queueDeclare("ORIGIN_QUEUE", false, false, false, null);
	
	        try {
	            channel.txSelect();
	            // 发送消息
	            // String exchange, String routingKey, BasicProperties props, byte[] body
	            channel.basicPublish("", "ORIGIN_QUEUE", null, (msg).getBytes());
	            // int i =1/0;
	            channel.txCommit();
	            System.out.println("消息发送成功");
	        } catch (Exception e) {
	            channel.txRollback();
	            System.out.println("消息已经回滚");
	        }
	
	        channel.close();
	        conn.close();
	    }
要在spring boot里设置:
	rabbitTemplate.setChannelTransacted(true);
2、Confirm(确认)模式

    Confirm模式既可以保证消息被Broker接收,有不大量消耗性能的方式。

①、普通确认模式

    在生产者调用channel.confirmSelect();方法将信道设置为Confirm模式,然后发送消息。一旦消息被投递到所匹配的队列后,RabbitMQ就会发送一个应答(ACK)给生产者。调用 channel.waitForConfirms()返回true,这样生产者就知道消息被服务端接收了。

		public static void main(String[] args) throws Exception {
		        ConnectionFactory factory = new ConnectionFactory();
		        factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));
		
		        // 建立连接
		        Connection conn = factory.newConnection();
		        // 创建消息通道
		        Channel channel = conn.createChannel();
		
		        String msg = "Hello world, Rabbit MQ ,Normal Confirm";
		        // 声明队列(默认交换机AMQP default,Direct)
		        // String queue, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
		        channel.queueDeclare("QUEUE_NAME", false, false, false, null);
		
		        // 开启发送方确认模式
		        channel.confirmSelect();
		
		        channel.basicPublish("", "QUEUE_NAME", null, msg.getBytes());
		        // 普通Confirm,发送一条,确认一条
		        if (channel.waitForConfirms()) {
		            System.out.println("消息发送成功" );
		        }
		
		        channel.close();
		        conn.close();
		    }
②、批量确认模式

    普通确认模式是发送一条确认一条,这样的效率还是不太高,因此有的批量确认方式。在生产者开启Confirm模式后,先发送一批消息,只要channel.waitForConfirmsOrDie()方法没有抛异常,则表示消息都被服务端接收。

		public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));

        // 建立连接
        Connection conn = factory.newConnection();
        // 创建消息通道
        Channel channel = conn.createChannel();

        String msg = "Hello world, Rabbit MQ ,Batch Confirm";
        // 声明队列(默认交换机AMQP default,Direct)
        // String queue, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
        channel.queueDeclare("QUEUE_NAME", false, false, false, null);

        try {
            channel.confirmSelect();
            for (int i = 0; i < 5; i++) {
                // 发送消息
                // String exchange, String routingKey, BasicProperties props, byte[] body
                channel.basicPublish("", "QUEUE_NAME", null, (msg +"-"+ i).getBytes());
            }
            // 批量确认结果,ACK如果是Multiple=True,代表ACK里面的Delivery-Tag之前的消息都被确认了
            // 比如5条消息可能只收到1个ACK,也可能收到2个(抓包才看得到)
            // 直到所有信息都发布,只要有一个未被Broker确认就会IOException
            channel.waitForConfirmsOrDie();
            System.out.println("消息发送完毕,批量确认成功");
        } catch (Exception e) {
            // 发生异常,可能需要对所有消息进行重发
            e.printStackTrace();
        }

        channel.close();
        conn.close();
    }
③异步确认模式

    批量确认方式虽然比单条效率要高,但是其确定是的是数量,不会准确到单条信息。对于不通的业务要多少条消息确定一次?太少的话影响效率,太多的话失败一条则所有的都要重发。因此出来了异步确认模式。
    异步确认模式需要添加一个ConfirmListener,并且用一个SortedSet来维护没有被确认的消息。
配置:

@Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback(){
            public void returnedMessage(Message message,
                                        int replyCode,
                                        String replyText,
                                        String exchange,
                                        String routingKey){
                System.out.println("回发的消息:");
                System.out.println("replyCode: "+replyCode);
                System.out.println("replyText: "+replyText);
                System.out.println("exchange: "+exchange);
                System.out.println("routingKey: "+routingKey);
            }
        });

        rabbitTemplate.setChannelTransacted(true);

        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                if (!ack) {
                    System.out.println("发送消息失败:" + cause);
                    throw new RuntimeException("发送异常:" + cause);
                }
            }
        });



        return rabbitTemplate;
    }

生产者:

		public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));

        // 建立连接
        Connection conn = factory.newConnection();
        // 创建消息通道
        Channel channel = conn.createChannel();

        String msg = "Hello world, Rabbit MQ, Async Confirm";
        // 声明队列(默认交换机AMQP default,Direct)
        // String queue, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
        channel.queueDeclare("QUEUE_NAME", false, false, false, null);

        // 用来维护未确认消息的deliveryTag
        final SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());

        // 这里不会打印所有响应的ACK;ACK可能有多个,有可能一次确认多条,也有可能一次确认一条
        // 异步监听确认和未确认的消息
        // 如果要重复运行,先停掉之前的生产者,清空队列
        channel.addConfirmListener(new ConfirmListener() {
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("Broker未确认消息,标识:" + deliveryTag);
                if (multiple) {
                    // headSet表示后面参数之前的所有元素,全部删除
                    confirmSet.headSet(deliveryTag + 1L).clear();
                } else {
                    confirmSet.remove(deliveryTag);
                }
                // 这里添加重发的方法
            }
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                // 如果true表示批量执行了deliveryTag这个值以前(小于deliveryTag的)的所有消息,如果为false的话表示单条确认
                System.out.println(String.format("Broker已确认消息,标识:%d,多个消息:%b", deliveryTag, multiple));
                if (multiple) {
                    // headSet表示后面参数之前的所有元素,全部删除
                    confirmSet.headSet(deliveryTag + 1L).clear();
                } else {
                    // 只移除一个元素
                    confirmSet.remove(deliveryTag);
                }
                System.out.println("未确认的消息:"+confirmSet);
            }
        });

        // 开启发送方确认模式
        channel.confirmSelect();
        for (int i = 0; i < 10; i++) {
            long nextSeqNo = channel.getNextPublishSeqNo();
            // 发送消息
            // String exchange, String routingKey, BasicProperties props, byte[] body
            channel.basicPublish("", "QUEUE_NAME", null, (msg +"-"+ i).getBytes());
            confirmSet.add(nextSeqNo);
        }
        System.out.println("所有消息:"+confirmSet);

        // 这里注释掉的原因是如果先关闭了,可能收不到后面的ACK
        //channel.close();
        //conn.close();
        }

2、消息从交换机路由到队列

    第二个环节就是从交换机路由到队列。消息无法路由到正确的队列的原因有 1)、路由键错误 2)、队列不存在。
    有两种方式处理无法路由的消息,一种是让服务器重发给生产者,一种是让交换机路由到另一个备份的交换机。

1)、消息回发的方式

    使用mandatory参数和ReturnListener(在Spring AMQP中是ReturnCallback)。

	@Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback(){
            public void returnedMessage(Message message,
                                        int replyCode,
                                        String replyText,
                                        String exchange,
                                        String routingKey){
                System.out.println("回发的消息:");
                System.out.println("replyCode: "+replyCode);
                System.out.println("replyText: "+replyText);
                System.out.println("exchange: "+exchange);
                System.out.println("routingKey: "+routingKey);
            }
        });
2)、消息路由到备份交换机的方式

    在创建交换机的时候,从属性中指定备份交换机。

	// 在声明交换机的时候指定备份交换机
     Map<String,Object> arguments = new HashMap<String,Object>();
     arguments.put("alternate-exchange","ALTERNATE_EXCHANGE");
     channel.exchangeDeclare("TEST_EXCHANGE","topic", false, false, false, arguments);

注意:队列可以指定死信交换机;交换机可以指定备份交换机

3、消息在队列存储

    第三个环节是消息在队列存储,如果没有消费者的话,队列会一直存在数据库中。
    如果RabbitMQ的服务或者硬件发生故障,可能会导致内存中的消息丢失,所以要把消息本身和元数据(队列、交换机、绑定)都保存到磁盘中。
    在RabbitConfig中配置

1)、队列持久化
@Bean("GpQueue")
public Queue QueueGpQueue(){
	//queueName,durable,exclusive,autoDelete,Properties
	return new Queue("GP_TEST_QUEUE",true,false,false,newHashMap<>());
}
2)、交换机持久化
@Bean("GpExchange")
public DirectExchange exchange(){
	//exchangeName,durable,exclusive,autoDelete,Properties
	return new DirectExchange("GP_TEST_EXCHANGE",true,false,newHashMap<>());
}
3)、消息持久化
public class ProducerApp {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ProducerApp.class);

        RabbitAdmin rabbitAdmin = context.getBean(RabbitAdmin.class);
        RabbitTemplate rabbitTemplate = context.getBean(RabbitTemplate.class);

        rabbitAdmin.declareExchange(new DirectExchange("GP_RELIABLE_SEND_EXCHANGE", true, false, new HashMap<>()));

        MessageProperties messageProperties = new MessageProperties();
        // 消息持久化
        messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
        messageProperties.setContentType("UTF-8");
        Message message = new Message("哈哈哈哈哈".getBytes(), messageProperties);

        rabbitTemplate.send("GP_RELIABLE_SEND_EXCHANGE", "gupao.tech", message, new CorrelationData("201906180001"));
        rabbitTemplate.send("GP_RELIABLE_SEND_EXCHANGE", "gupao.tech.wrong", message, new CorrelationData("201906180002"));
    }
}
4)、集群

    如果只有一个RabbitMQ节点,即使交换机、队列、消息持久化,如果服务崩溃或者硬件发生故障,其服务一样是不可用的,为了提高MQ服务的可用性,保障消息的传输,则需要搭建多个节点。

4、消息投递到消费者

    如果消费者收到消息后没来得及处理便发生异常,或者处理过程中中断,会导致④失败。服务端应该以某种方式得知消费者对消息的接收情况,并决定是否重新投递这条消息给其他消费者。
    RabbitMQ提供了消费者的消息确认机制(message acknowledgement),消费者可以自动或者手动发送ACK给服务端。
    没有收到ACK的消息,消费者断开连接后,当autoAck,RabbitMQ会把这条消息发送给其他消费者。如果没有其他消费者,消费者重启后会重新消费这条消息,重复执行业务逻辑。
    消费者在订阅队列时,可以指定autoAck参数,当autoAck等于false时,RabbitMQ会等待消费者显式的回复确认信号后才会从对垒中移去消息。
    手动设置ACK
SimpleRabbitListenerContainer或者SimpleRabbitListenerContainerFactory

factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);

application.properties

spring.rabbitmq.listener.direct.acknowledge-mode=manual
spring.rabbitmq.listener.simple.acknowledge-mode=manual

注意:
    NONE:自动ACK
    MANUAL:手动ACK
    AUTO:如果方法未抛出异常,则发送ack。
    当抛出AmqpRejectAndDontRequeueException异常的时候,则消息会被拒绝,且不重新入队。当抛出ImmediateAcknowledgeAmqpException异常,则消费者会发送ACK。其他的异常,则消息会被拒绝,且requeue=true会重新入队。

    如果消息无法处理或者消费失败,也有两种拒绝的方式,Basic.Reject()拒绝单条,Basic.Nack()批量拒绝。如果requeue参数设置为true,可以把这条消息重新存入队列,以便发给下一个消费者(当然,只有一个消费者的时候,这种方式可能会出现无限循环重复消费的情况。可以投递到新的队列中,或者只打印异常日志)。

5、消费者回调

1)、调用生产者API

    例如:提单系统给其他系统发送了碎屏保消息后,其他系统必须在处理完消息后调用提单系统提供的API,来修改提单系统中数据的状态。只要API没有被调用,数据状态没有被修改,提单系统就认为下游系统没有收到这条消息。

2)、发送响应消息给生产者

    例如:商业银行与人民银行二代支付通信,无论是人行收到了商业银行的消息,还是商业银行收到了人行的消息,都必须发送一条响应消息(叫做回执报文)。

6、补偿机制

    如果生产者的API就是没有被调用,也没有收到消费者的响应消息,怎么办?
    不要着急,可能是消费者处理时间太长或者网络超时。
    生产者与消费者之间应该约定一个超时时间,比如5分钟,对于超出这个时间没有得到响应的消息,可以设置一个定时重发的机制,但要发送间隔和控制次数,比如每隔2分钟发送一次,最多重发3次,
    否则会造成消息堆积。
    重发可以通过消息落库+定时任务来实现。
比如:
    ATM机上运行的系统叫C端(ATMC),前置系统叫P端(ATMC),它接收ATMC的消息,再转发给卡系统或者核心系统。
    1)、如果客户存款,没有收到核心系统的应答,不知道有没有记账成功,最多发送5次存款确认报文,因为已经吞钞了,所以要保证成功;
    2)、如果客户取款,ATMC未得到应答时,最多发送5次存款冲正报文。因为没有吐钞,所以要保证失败。

7、消息幂等性

    如果消费者每一次接收生产者的消息都成功了,只是在响应或者调用API的时候出了问题,会不会出现消息的重复处理?例如:存款100元,ATM重发了5次,核心系统一共处理了6次,余额增加了600元。所以,为了避免相同消息的重复处理,必须要采取一定的措施。RabbitMQ服务端是没有这种控制的(同一批的消息有个递增的DeliveryTag)
,它不知道你是不是就要把一条消息发送两次,只能在消费端控制。
    如何避免消息的重复消费?
    消息出现重复可能会有两个原因:
    1、生产者的问题,环节①重复发送消息,比如在开启了Confirm模式但未收到确认,消费者重复投递。
    2、环节④出了问题,由于消费者未发送ACK或者其他原因,消息重复投递。
    3、生产者代码或者网络问题。对于重复发送的消息,可以对每一条消息生成一个唯一的业务ID,通过日志或者消息落库来做重复控制。

8、消息的顺序性

    在RabbitMQ中,一个队列有多个消费者时,由于不同的消费者消费消息的速度是不一样的,顺序无法保证。只有一个队列仅有一个消费者的情况才能保证顺序消费(不同的业务消息发送到不同的专用的队列)。

你可能感兴趣的:(技术,消息中间件,rabbitmq,分布式)