RabbitMQ 可靠性投递 (引用)

7.RabbitMQ可靠性投递

7.1.解释

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

7.2.解决

7.2.1.前提

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

7.2.2.工作模式

在这里插入图片描述

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

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

7.3.各个环节处理

7.3.1.消息发送到服务器

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

7.3.1.1.Transaction(事务)模式

在这里插入图片描述

我们可以通过一个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<String, Object> 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);
7.3.1.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<String, Object> 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<String, Object> 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<String, Object> 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();
    }

7.3.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);

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

7.3.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服务的可用性,保障消息的传输,则需要搭建多个节点。

7.3.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,可以把这条消息重新存入队列,以便发给下一个消费者(当然,只有一个消费者的时候,这种方式可能会出现无限循环重复消费的情况。可以投递到新的队列中,或者只打印异常日志)。

7.4.消费者回调

1)、调用生产者API

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

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

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

7.5.补偿机制

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

7.6.消息幂等性

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

7.7.消息的顺序性

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值