服务异步通讯高级篇一(消息可靠性)

服务异步通讯高级篇一(消息可靠性)

消息队列在使用过程中,面临着很多实际问题需要思考:

消息从生产者发送到交换机(exchange),再到队列(queue),再到消费者,有哪些导致消息丢失的可能性?
  1. 发送时丢失:
    • 生产者发送的消息为发送发哦exchange
    • 消息到达了exchange未到queue
  2. MQ宕机,queue将消息丢失
  3. consumer接收到消息后为消费就宕机

1、生产者消息确认

RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。这种机制必须给每个消息指定一个唯一ID。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功。

返回结果有两种方式:

  • publisher-confirm,发送者确认
    • 消息成功投递到交换机,返回ack
    • 消息未投递到交换机,返回nack
  • publisher-return,发送者回执
    • 消息投递到交换机了,但是没有路由到队列。返回ACK,及路由失败原因。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9t4EdocU-1654051739850)(images/image-20220531212021050.png)]

注意:确认机制发送消息时,需要给每一个消息设置一个全局的唯一id,以区分不同的消息,避免不同的ack冲突
1.1、项目准备

创建一个mq-advanced-demo聚合的工程,里面再创建两个子模块,都是基于springboot的开发,环境的搭建与之前的一直

githun地址:https://github.com/ZhongChunle/mq-advanced-demo.git

1.2、SpringAMQP实现生产确认
  1. publisher模块中的application.yml文件中添加配置信息
    spring:
      rabbitmq:
        publisher-confirm-type: correlated
        publisher-returns: true
        template:
          mandatory: true
    

    说明:

    • publish-confirm-type:开启publisher-confirm,这里支持两种类型:
      • simple:同步等待confirm结果,直到超时
      • correlated:异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback
    • publish-returns:开启publish-return功能,同样是基于callback机制,不过是定义ReturnCallback
    • template.mandatory:定义消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息
  2. 定义Return回调

    每个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目加载时配置:

    修改publisher服务,添加一个:

    package cn.itcast.mq.config;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.amqp.core.Message;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
    import org.springframework.amqp.support.converter.MessageConverter;
    import org.springframework.beans.BeansException;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ApplicationContextAware;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Slf4j
    @Configuration
    public class CommonConfig implements ApplicationContextAware {
    
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            // 1、获取rabbittemplate对象
            RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
            // 2、配置ReturnCallback【使用了lambda表达式】
            rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
                // 投递失败,记录日志
                log.info("消息发送到队列失败,应答码{},失败原因{},交换机{},路由键key{},消息{}",
                        replyCode, replyText, exchange, routingKey, message.toString());
                // 如果有业务需要,可以重发消息
            });
        }
    }
    
    
    IDEA对于可简化的代码可以按照键盘的Alt + 回车进行一个lambda简化,鼠标需要放到可以简化的位置才会生效
  3. 定义ConfirmCallback

    ConfirmCallback可以在发送消息时指定,因为每个业务处理confirm成功或失败的逻辑不一定相同。

    在publisher服务的cn.itcast.mq.spring.SpringAmqpTest类中,定义一个单元测试方法:

    package cn.itcast.mq.spring;
    
    import lombok.extern.slf4j.Slf4j;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.amqp.rabbit.connection.CorrelationData;
    import org.springframework.amqp.rabbit.core.RabbitTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.util.concurrent.FailureCallback;
    import org.springframework.util.concurrent.SuccessCallback;
    
    import java.util.UUID;
    
    @Slf4j
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class SpringAmqpTest {
        @Autowired
        private RabbitTemplate rabbitTemplate;
    
        @Test
        public void testSendMessage2SimpleQueue() throws InterruptedException {
            String routingKey = "simple.test";
            // 1、准备消息
            String message = "hello, spring amqp!";
            // 2、准备correlationData
            // 2.1、消息id
            CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
            // 2.2、准备ConfirmCallback
            correlationData.getFuture().addCallback(confirm -> {
                // 判断结果
                if(confirm.isAck()){
                    // ACK
                    log.info("消息投递到交换机成功!消息ID:{}",correlationData.getId());
                }else{
                    // NCAK
                    log.info("消息投递到交换机失败,消息ID:{}"+correlationData.getId());
                }
            }, throwable -> {
                // 记录日志
                log.info("消息发送失败:"+ throwable);
                // 重发消息
            });
            // 3、发送消息
            rabbitTemplate.convertAndSend("zcl.topic", routingKey, message);
        }
    }
    
    

    点击启动消息方法,查看管理页面的消息队列是否有消息

目前的上面测试有一个IDEA问题就是,我将队列名弄错了可以在控制台输出(log.info)的队列错误日志,但是我将交换机弄错了,就是上面设置的SpringAmqpTest中的(log.info)日志就输出不了,我也不知道是啥子原因,有细心留意同伴请给我留言!

2、消息持久化

MQ默认是内存存储消息,开启持久化功功能可有确保缓存在MQ中的消息不会丢失

重启交换机

docker restart [容器名称或id]

重启之后的rabbitmq的队列消息都会消失,而系统的交换机都会存在,而想要交换机持久化或队列持久化就需要通过管理页面或者java代码进行设置了,下面就是在管理页面创建的时候的一个创建持久化的一个方法(队列也是一样的,创建的时候选上这个持久化选项就可以了)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cTUo2a3b-1654051739851)(images/12、持久化.png)]

交换机持久化设置

在消费者consumer中创建

package cn.itcast.mq.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CommonConfig {

    /**
     * 创建持久化交换机
     * @return
     */
    @Bean
    public DirectExchange simpleDirect() {
        // 第一个参数:交换机名称  第二个参数:是否开启持久化  第三个参数:是否自动删除
        return new DirectExchange("simpl.direct",true,false);
    }
}

队列消息持久化设置

也是在消费者中创建,接上上面创建交换机的下面

/**
 * 创建持久化的队列
 */
public Queue simpleQueue() {
    return QueueBuilder.durable("simple.queue").build();
}

启动消费者模块,回到管理页面查看交换机和队列的创建是否有创建成持久化【一个D标识】

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K1ebXJYO-1654051739852)(images/13、持久化.png)]

可以点击进去给队列添加信息,然后重启MQ再次查看,队列会持久而我们的消息展示不会持久

消息持久化设置

在消息生产者中设置消息持久化设置,发送消息的时候要想将消费者的服务停止掉,否则的化发送的消息一下就被消费了,那么在管路页面是不会再看到发送的数据的

/**
 * 发送消息持久化
 */
@Test
public void testDueableMessage(){
    // (MessageDeliveryMode.PERSISTENT:设置消息持久
    Message build = MessageBuilder.withBody("hellow spring".getBytes(StandardCharsets.UTF_8))
            .setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();
    // 发送消息,指定通道
    rabbitTemplate.convertAndSend("simple.queue",build);
}

查看管理页面队列消息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6hBvGZZA-1654051739852)(images/14、消息持久化.png)]

重启MQ再次访问是否真正的持久化了

在SpringAMQP中的【队列、交换机、消息】默认情况下都是持久的

3、消费者消息确认

RabbitMQ是阅后即焚机制,RabbitMQ确认消息被消费者消费后会立刻删除。

而RabbitMQ是通过消费者回执来确认消费者是否成功处理消息的:消费者获取消息后,应该向RabbitMQ发送ACK回执,表明自己已经处理消息。

设想这样的场景:

  • 1)RabbitMQ投递消息给消费者
  • 2)消费者获取消息后,返回ACK给RabbitMQ
  • 3)RabbitMQ删除消息
  • 4)消费者宕机,消息尚未处理

这样,消息就丢失了。因此消费者返回ACK的时机非常重要。

而SpringAMQP则允许配置三种确认模式:

•manual:手动ack,需要在业务代码结束后,调用api发送ack。

•auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack

•none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除

由此可知:
  • none模式下,消息投递是不可靠的,可能丢失
  • auto模式类似事务机制,出现异常时返回nack,消息回滚到mq;没有异常,返回ack
  • manual:自己根据业务情况,判断什么时候该ack

一般,我们都是使用默认的auto即可。

在消费者中的配置文件中设置
spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: none # 关闭ack

如果需要观察的,就根据下面的断点来查看没一个模式的销毁成功,测试了一个再换一个模式进行测试

案例模拟一下消息异常
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue(String msg) {
    log.info("消费者接收到simple.queue的消息:【{}】", msg);
    // 模拟异常
    System.out.println(1 / 0);
    log.debug("消息处理完成!");
}

启动测试,通过断电5行返回查看管理页面的队列消息,是否销毁

如果是auto模式的话当遇到报错就会出现下面的情况,然后不停的发送消息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F9nO6Chd-1654051739853)(images/image-20220601095245582.png)]

4、消费失败本地重试机制

当消费者出现异常后,消息会不断requeue(重入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PrP0wI0R-1654051739854)(images/image-20220601095724870.png)]

我们可以利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列。

修改consumer服务的application.yml文件,添加内容:

spring:
  rabbitmq:
    listener:
      simple:
        retry:
          enabled: true # 开启消费者失败重试
          initial-interval: 1000 # 初识的失败等待时长为1秒
          multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
          max-attempts: 3 # 最大重试次数
          stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false

模拟错误接收消费

@Slf4j
@Component
public class SpringRabbitListener {

    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg) {
        log.debug("消费者接收到simple.queue的消息:【" + msg + "】");
        System.out.println(1/0);
        log.info("消费者处理消息成功");
    }
}

重启consumer服务,重复之前的测试。可以发现:

  • 在重试3次后,SpringAMQP会抛出异常AmqpRejectAndDontRequeueException,说明本地重试触发了
  • 查看RabbitMQ控制台,发现消息被删除了,说明最后SpringAMQP返回的是ack,mq删除消息了
默认情况下,本地重试耗尽之后会把消息丢弃,返回一个reject拒绝,消息就会丢弃,这是SpringAMQP的默认机制
结论:
  • 开启本地重试时,消息处理过程中抛出异常,不会requeue到队列,而是在消费者本地重试
  • 重试达到最大次数后,Spring会返回ack,消息会被丢弃

5、失败策略

在之前的测试中,达到最大重试次数后,消息会被丢弃,这是由Spring内部机制决定的。

在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecovery接口来处理,它包含三种不同的实现:

  • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式

  • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队

  • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机

比较优雅的一种处理方案是RepublishMessageRecoverer,失败后将消息投递到一个指定的,专门存放异常消息的队列,后续由人工集中处理。

1)在consumer服务中定义处理失败消息的交换机和队列

package cn.itcast.mq.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 项目名称:mq-advanced-demo
 * 描述:覆盖默认的重试策略
 *
 * @author zhong
 * @date 2022-06-01 10:17
 */
@Configuration
public class ErrorMessages {

    /**
     * 定义交换机
     */
    @Bean
    public DirectExchange erroeMessages() {
        return new DirectExchange("error.direct");
    }

    /**
     * 定义队列
     */
    @Bean
    public Queue errorQueue() {
        return new Queue("error.queue");
    }

    /**
     * 绑定交换机和队列
     */
    @Bean
    public Binding errorMessagesBinding() {
        return BindingBuilder.bind(errorQueue()).to(erroeMessages()).with("error");
    }
}

2)定义一个RepublishMessageRecoverer,关联队列和交换机

接着上一段代码

定义的交换机名称和routingKey一定要与上面代码定义的一直

@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
    return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}

3)启动项目测试

  1. 目前启动的消费者中有一个接收消息的,同时在接收消息后面添加了一个自杀死错误,用来默认接收消息重试失败的

  2. 在管理页面手动的给队列发送消息

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QALAa4BL-1654051739854)(images/15、管理页面发送.png)]

  3. 重新查看IDEA的打印

    10:28:54:768  INFO 16296 --- [           main] cn.itcast.mq.ConsumerApplication         : Started ConsumerApplication in 1.024 seconds (JVM running for 1.434)
    10:30:29:567 DEBUG 16296 --- [ntContainer#0-1] c.i.mq.listener.SpringRabbitListener     : 消费者接收到simple.queue的消息:【hello error】
    10:30:30:568 DEBUG 16296 --- [ntContainer#0-1] c.i.mq.listener.SpringRabbitListener     : 消费者接收到simple.queue的消息:【hello error】
    10:30:31:572 DEBUG 16296 --- [ntContainer#0-1] c.i.mq.listener.SpringRabbitListener     : 消费者接收到simple.queue的消息:【hello error】
    10:30:32:576 DEBUG 16296 --- [ntContainer#0-1] c.i.mq.listener.SpringRabbitListener     : 消费者接收到simple.queue的消息:【hello error】
    10:30:32:580  WARN 16296 --- [ntContainer#0-1] o.s.a.r.retry.RepublishMessageRecoverer  : Republishing failed message to exchange 'error.direct' with routing key error
    

    在接受四次重试后就会将消息妆发到error.direct交换机,也就是我们声明的交换机,同时key为error

  4. 再次查看我们的管理页面

    原来队列的信息以及销毁了,而我们的错误信息队列保存销毁的信息

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NC7IKNZx-1654051739855)(images/16、error获取信息.png)]

    点击队列可以查看到清楚的错误消息以及消息

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CBev0FWZ-1654051739856)(images/17、错误原因.png)]

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小钟要学习!!!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值