Rabbit MQ 和Spring AMQP学习三(消息的可靠性)

Rabbit MQ 和Spring AMQP学习三(消息的可靠性)

在实际生产环境中,各种意想不到的情况都可能会发生。那么如何尽最大努力保证Producer端生产的消息要被Consumer端消费掉呢?也就是两方面,消息发送的可靠性,消息消费的可靠性。

Producer端对于消息投递的可靠性

Rabbit MQ消息模型中,三大角色就是Producer、Broker、Consumer.那么首先要确保,生产者发送的消息要能够到达Broker。生产者负责生产消息,并要确保该消息成功到达Broker。Broker是存在于Rabbit MQ Server上的,并且Broker包含了exchange,binding,queue.那么一步一步分析可能会出现问题的地方

Spring AMQP官方文档也给出了可能会出现问题的情况

可能会出现问题的地方

  1. Producer向Rabbit MQ Server发送消息的过程中,因为网络原因,并未到达Rabbit MQ Server
  2. Producer的消息到达了Rabbit MQ Server,开始寻找对应的Exchange.找不到对应的Excahnge.换种说法,Exchange不存在。此时Rabbit MQ Server如何处理消息?
  3. Producer的消息到达了Rabbit MQ Server,并到达了对应的Exchange,但是通过routingKey和binding,无法找到对应的Queue.此时Rabbit MQ Server又是如何处理消息?

解决措施

针对上述三个生产者端可能会发生的问题,逐一进行解决。

针对第一点,如果因为网络原因,Producer无法和Rabbit MQ Server建立有效连接。那么消息直接发送失败。

针对第二、第三,Spring AMQP提供了publish-confirm模式。此模式的相关官方文档1官方文档2

如何确定消息能否到达对应的Exchange

针对第二点,我们需要当消息到达Server的第一站-Exchange的时候,Rabbit MQ Server通知下我们,告知我们Rabbit MQ Server已经找到了对应的Exchange

Spring AMQP提供了RabbitTemplate.setConfirmCallback方法.Spring AMQP对于Producer端的确认支持,需要使用CachingConnectionFactory。并且有两个必要条件,官方文档

  1. PublisherReturns要设置为true
  2. PublisherConfirmType设置为ConfirmType.CORRELATED
@Bean
    public ConnectionFactory publishConnectionFactory() {
        CachingConnectionFactory result = new CachingConnectionFactory();
        result.setConnectionNameStrategy(connectionFactory -> "publishConnection");
        result.setHost("localhost");
        result.setPort(5672);
        result.setUsername("dhb");
        result.setPassword("123456");
        result.setVirtualHost("dhb");
        result.setCacheMode(CacheMode.CONNECTION);
        result.setPublisherReturns(true);
        result.setPublisherConfirmType(ConfirmType.CORRELATED);
        return result;
    }

配置RabbitTemplte的setConfirmCallback方法

@Bean
    public RabbitTemplate publishConfirmRabbitTemplate() {
        RabbitTemplate result = new RabbitTemplate(publishConnectionFactory());
        // 第一步:判断Producer -> Exchange 是否有问题
        // 要想使用correlationData,则发送时要设置该值,否则是null,设置方法见{@link com.example.rabbitmq.rabbitmqdemo.producer.TopicProducer.sendTopicMessage}
        result.setConfirmCallback((correlationData, ack, cause) -> {
            if (ack) {
                log.info("能够被Rabbit MQ Server的exchange接收到......");
            }else {
                log.info("在Rabbit MQ Server上找不到对应的exchange......原始信息id是{}, 原始信息是{}, 原因是{}" ,
                    Objects.requireNonNull(correlationData).getId(),
                    // 获取到body体的字节数组,直接只用 new String(byte[])构造方法,将字节数组转为字符串
                    new String(Objects.requireNonNull(correlationData.getReturnedMessage()).getBody()),
                    cause);
            }
        });
        return result;
    }

setConfirmCallback是一个函数式接口,包含了三个参数.

第一个参数correlationData是RabbitTemplate.converAndSend()方法时传的参数,如果没传,则获取的是null.

第二个参数ack则代表是否找到了对应的Exchange,如果为true,则代表找到了,如果为false,则代表没找到

第三个参数cause代表未找到Exchange的原因。当未找到Exchange时,该消息会被丢弃
在这里插入图片描述

模拟异常情况,发送时向不存在的Exchange发送消息
在这里插入图片描述
在这里插入图片描述

到达Exchange之后,如何确定消息可以到达对应的Queue

在Rabbit MQ 消息模型中,Exchange 只负责转发消息,是不负责存储消息的。存储消息的工作由Queue来做。因此,Producer端除了要保证消息能够找到正确的,已存在的Exchange,还要保证,Exchange能够转发到对应的Queue上。如果找不到对应的Queue,那么如何通知Producer端。

Spring AMQP提供了setReturnsCallback。

使用returnsCallback的前提是RabbitTemplate的mandatory为true.继续在上面的RabbitTemplate配置中配置returnsCallback方法

....前面的省略
// 第二步:判断Exchange -> Queue 是否有问题
        // 设置为true,在exchange找不到queue的时候才会回调
        result.setMandatory(true);
        result.setReturnsCallback(returned -> {
            log.info("找到了exchange,但是没有被路由到正确的queue....routingKey是:{}, 原始信息是{}", returned.getRoutingKey(), new String(returned.getMessage().getBody()));
        });

setReturnsCallback也是一个函数式接口,包含了一个ReturnedMessage对象,该对象有五个属性

第一个属性Message,Producer发出的原始消息

第二个属性replyCode, 回调的原因码

第三个属性replyText,回到原因的字符串

第四个属性exchange,发送消息时携带的exchange的名字

第五个属性routingKey,发送消息时携带的routingKey

正常情况下,如果routingKey能够和Binding对应上,是不会触发此回调。只有在找不到对应的Queue的时候,才会触发

模拟异常情况,这次exchange设置正确,Queue设置错误
在这里插入图片描述

可以看到在没有对应的Queue的情况下,回调通知到了Producer端,Producer端可根据实际情况进行处理

Server端对于消息保存的可靠性

前面,通过Spring AMQP提供的方法,我们可以保证,在Producer端发送消息时,如果消息没有被正确接收(没到达正确的Queue之前都属于未被正确接收),Producer端可以收到Rabbit MQ Server的相关回调通知。在Producer端来自主决定是消息重发、还是不再发送一系列的逻辑。

而我们做开发的日常思考最多的问题就是,如果服务器宕机了怎么办?对于Rabbit MQ Server,如果Rabbit MQ Server来说,如果它宕机了,那么在宕机时,可能Server上存在着大量的未消费的消息。怎么办?

Server端Broker的相关组件要可重新加载

首先,如果Server宕机了,且不说存量消息怎么办。Server重启之后,如何保证原来的Server端的转发逻辑、存储逻辑依然有效?

这里转发逻辑有效是指,Server重启后,新到达的正确的消息,能否找到Exchange,根据Exchange和binding、routingKey又如何找到Queue.

存储逻辑是指能否能像未宕机之前一样找到对应的Queue并存储。

那么要保证这两个逻辑经过N次Server重启依然有效的办法就是Broker持久化。当Rabbit MQ Server重启的时候,只需要根据机器上持久化的文件重新加载相关组件即可。

那么我们需要整理下Broker哪些部分需要持久化?

  1. Exchange
  2. Queue

这两部分是构成了Broker的关键,并且互相关联,缺一不可。所以,这两个都需要持久化。对于Excahnge、Queue,Spring AMQP都提供了durable属性,true代表需要持久化。官方文档
在这里插入图片描述

对于消息的可持久化

上面的设置保证了Rabbit MQ Server在宕机重启之后,对于新到达的消息依旧可以想宕机之前一样处理。那么对于宕机之前的未消费的消息、正在消费但是未确认消费成功的消息怎么办?

如何保证宕机重启之后,这些存量数据还在,那么就要把消息存到Rabbit MQ Server所在机器的硬盘上,当Server重启后,读取具体存储位置下未消费的消息数据即可,官方术语是消息持久化

Producer端发送消息时,需要告知Server端,此消息需要持久化,Spring AMQP默认的Message对象中的MessagePorperties对象中的DeliverMode是PERSISTENT-持久化的。

手动设置消息可持久化、非持久化

 MessageProperties messageProperties = new MessageProperties();
        messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
        Message message = new Message("this is persistent message".getBytes(), messageProperties);
        rabbitTemplate.convertAndSend(topicExchange.getName(), "message.second.Topic", message);

而Rabbit 官方教程的第二个教程也给出了消息持久化的对应API和说明
在这里插入图片描述

但是,同时下面官方还有一段比较着重的话
在这里插入图片描述

Rabbit MQ Server并不完全保证,消息不会丢失。因为在开启了消息持久化的时候,Server可能还没有真正写到机器的硬盘上,可能还在操作系统的cache中。这个时候,如果机器的操作系统崩溃了,其实该消息也是丢失了。

Consumer端对于消息的消费的可靠性

实际使用MQ过程中,在消费者端处理消息时,往往有一些业务逻辑要处理。我们认为实际业务逻辑正确地处理完才算成功消费掉了一条消息。而实际上,默认情况下,Spring AMQP是自动确认消息的,消息刚到达@RabbitListener注解的消费者端,该消息就被标记为成功消费并返回通知Rabbit MQ Server.这种处理方式和我们想要的方式不一样。我们采取手动确认消息,当处理完实际业务逻辑之后,我们手动确认消息被成功消费。而处理业务逻辑过程中一旦失败,则我们手动确认消息消费失败

手动确认消息

首先,我们要设置消息的确认状态为手动。

Rabbit MQ官方关于消费端确认消息的相关文档.很详细的说明了为什么要确认消息?手动确认的使用方式等等

Rabbit官方的JAVA API

// 如果要手动确认消息消费成功,则直接调用
channel.ack(deliveryTag, true)// 如果要手动确认消息消费失败,则直接调用
channle.Nack(deliveryTag, true, true);
deliveryTag是干嘛的

deliveryTag是Rabbit MQ Server的推送消息标识。代表着此次推送的消息的唯一性。
在这里插入图片描述

其最大值为long类型的最大值2^63 -1.官方说明

Spring AMQP允许在@RabbitListener注解上设置ackMode类型.一共有三种

  1. NONE-自动确认
  2. MANUAL-手动确认
  3. AUTO-根据实际情况判断
@RabbitListener(queues = "${queue.topic.first}", ackMode = "MANUAL")

注意这里的ackMode对应的字符串一定要大写

也可以在ContainerFactory或者Container中设置消息确认模式

@Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory() {
        SimpleRabbitListenerContainerFactory result = new SimpleRabbitListenerContainerFactory();
        result.setConnectionFactory(connectionFactory());
        result.setPrefetchCount(2);
        // 这里根据实际情况来配置是否开启全局消息手动确认
        result.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        return result;
    }

根据上面的Rabbit MQ官方的JAVA API可知,如果需要手动确认,我们需要在Consumer端接收消息的时候,获取到此时和Rabbit MQ Server之间的channel对象。

而Spring AMQP对于手动确认消息消费成功,并没有提供很方便的回调方法之类的,只有统一的MessageListenerAdapter,如果自定义了MessageAdapter,并重写了buildListenerArguments方法,该方法里有上面所说的需要的channel对象。则可以使用Rabbit官方的JAVA API手动确认。详细文档

后来经过查阅资料,当我们的Consumer端的具体@RabbitListener注解标注的方法里有Channel参数时,可以自动获得。这样一来就方便多了。

示例代码

/**
     * 监听Queue的时候,直接获取消息体.
     * 在注解上开启手动确认, 必须是ackMode的大写.
     * 在进行消息确认的时候,要带上Rabbit MQ Server发送过来头上的tag,可以通过@Header注解获取delivery tag,
     * @param firstTopicQueueMessage 消息体
     * @param channel Broker和Consumer建立的channel
     * @param tag 消息头中的tag
     */
    @RabbitListener(queues = "${queue.topic.first}", ackMode = "MANUAL")
    @RabbitHandler
    @SneakyThrows
    public void receiveFirstTopicQueueMessage(String firstTopicQueueMessage, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
        log.info("This is firstTopicQueue received message: {}", firstTopicQueueMessage);
        try {
            // 处理实际业务
            doActualWork();
            TimeUnit.SECONDS.sleep(5);
            // 制造异常
            // int wrongNumber = 1/0;
            // 无异常,确认消息消费成功
            channel.basicAck(tag, true);
        }catch (IOException | ArithmeticException exception) {
            log.error("处理消息发生异常", exception);
            // 有异常,将消息返回给Queue里,第三个参数requeue可以直接看出来,是否返回到Queue中
            channel.basicNack(tag, true, true);
        }
    }

这里我们获取Rabbit MQ Server的deliveryTag可以通过Spring AMQP提供的注解获取。对于具体的消息体直接使用对应的类型-这里是字符串接收(也可以是个POJO)

还有另外一种方式获取消息,通过Message对象获取

/**
     * 监听Queue的时候,不直接获取消息体.而是通过Message对象获取消息体
     * @param secondTopicQueueMessage Message对象
     * @param channel Broker和Consumer建立的channel
     */
    @RabbitListener(queues = "${queue.topic.second}", ackMode = "MANUAL")
    @RabbitHandler
    @SneakyThrows
    public void receiveSecondTopicQueueMessage(Message secondTopicQueueMessage, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) {
        byte[] messageBody = secondTopicQueueMessage.getBody();
        String message = new String(messageBody, Charset.defaultCharset());
        log.info("This is secondTopicQueue received message: {}", message);
        // 开启休眠,可以在控制台上发现当前队列中的消息处于Unacked状态
        TimeUnit.SECONDS.sleep(60);
        channel.basicAck(deliveryTag, true);
    }

当开启了手动确认消息时,在我们没有手动确认消息之前,通过控制台观察,该消息都是Unacked的状态
在这里插入图片描述

当我们模拟异常发生时,通过代码debug,发现消息没有被认为是消费成功,而是又返回到Queue中,重新发送给消费者。

注意channel.basicNack不一定是消息处理失败才可以用,当Consumer端处理不过来消息,也可以直接拒绝确认消息。消息返回到Queue之后,如果,有其他Consumer,会发送给其他Consumer处理.Rabbit MQ官方文档说明如下
在这里插入图片描述

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值