rabbitMQ高级

目录

消息可靠性

消息可靠性 -- 生产者确认

消息可靠性 -- 消息持久化

消息可靠性 -- 消费者确认

消息可靠性 -- 消费者失败重试机制

消息可靠性 -- 实现总结

死信交换机

死信交换机 -- 实现消息延迟推送

死信交换机 -- 延迟队列插件 -- DelayExchange

消息堆积 -- 惰性队列

MQ集群分类

MQ集群 -- 普通集群

MQ集群 -- 镜像集群

MQ集群 -- 仲裁集群


消息可靠性

rabbitMQ高级_第1张图片

 在消息的推送,路由和接收处理中,都有可能遇到消息的丢失,常见的丢失原因可以分为三类:

        发送时丢失: 生产者发送的消息未送达exchange 消息到达exchange后未到达queue

        MQ宕机,queue中的消息丢失

        consumer接收到消息后未消费就宕机

针对这三类情况,MQ提供了四种解决方案:

        生产者确认机制

        消息持久化

        消费者确认机制

        失败重试机制

消息可靠性 -- 生产者确认

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

返回结果有两种方式:

        publisher-confirm,发送者确认

                消息成功投递到交换机,返回ack

                消息未投递到交换机,返回nack

        publisher-return,发送者回执

                消息投递到交换机了,但是没有路由到队列。返回ACK,及路由失败原因。

发送消息的时候需要给消息提供一个唯一ID,结果返回的时候可以绑定ack/nack, 用来区分不同的消息,避免ack碰撞


配置生产者确认模式:

        修改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:则直接丢弃消息


定义publish-returns的回调ReturnCallback:

        在spring中RabbitTemplate只能配置一个ReturnCallback,所以我们需要在启动时配置

@Slf4j
@Configuration
//继承ApplicationContextAware,spring的bean容器通知接口
public class CommonConfig implements ApplicationContextAware {

    //重写设置容器方法
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // 从bean容器中获取RabbitTemplate这个bean
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        // 通过lambda表达式设置ReturnCallback回调函数
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            // 投递失败,记录日志
            log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
                     replyCode, replyText, exchange, routingKey, message.toString());
            // 如果有业务需要,可以重发消息
        });
    }
}

 定义publisher-confirm的correlated类型回调ConfirmCallback:

//模拟发送消息
public void testSendMessage2SimpleQueue() throws InterruptedException {
    //构造消息体
    String message = "hello, spring amqp!";
    //因为Correlated类型的回调
    //所以需要创建CorrelationData对象   
    //需要封装两个属性 1:消息唯一id  2:回调函数              这里封装消息唯一ID
    CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    //封装callback回调
    correlationData.getFuture().addCallback(
        result -> {
            //调用函数判断MQ返回结果
            if(result.isAck()){
                //ack,消息成功
                log.debug("消息发送成功, ID:{}", correlationData.getId());
            }else{
                //nack,消息失败
                log.error("消息发送失败, ID:{}, 原因{}",correlationData.getId(), result.getReason());
            }
        },
        ex -> log.error("消息发送异常, ID:{}, 原因{}",correlationData.getId(),ex.getMessage())
    );
    //发送消息,参数: 交换机 routingKey  correlationData对象
    rabbitTemplate.convertAndSend("task.direct", "task", message, correlationData);

    // 休眠一会儿,等待ack回执
    Thread.sleep(2000);
}

消息可靠性 -- 消息持久化

生产者确认机制可以保证将消息推送到队列中,但是如果MQ宕机,还是会出现消息丢失问题,我们可以使用消息持久化机制解决这个问题

而消息持久化分为三个阶段:

        交换机持久化

        队列持久化

        消息持久化

而spring中,默认是全部实现持久化的

不过我们也可以手动设置,或者取消,因为持久化是写入到本地中的,会有额外的磁盘IO,所以我们可以根据实际情况,将一些低价值的数据取消持久化,实现和取消持久化,只需要设置direct 类型即可,true为开启持久化, false为取消持久化


交换机设置

@Bean
public DirectExchange simpleExchange(){
    // 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除
    return new DirectExchange("simple.direct", true, false);
}

队列设置

@Bean
public Queue simpleQueue(){
    // 使用QueueBuilder构建队列,durable就是持久化的
    return QueueBuilder.durable("simple.queue").build();
}

消息设置

rabbitMQ高级_第2张图片

消息可靠性 -- 消费者确认

值得注意的是RabbitMQ是"阅后即焚"模式的,即消费者确认接收到消息后,MQ就会将消息删除,那如果此时消费者宕机,消息就会丢失,所以针对消费者确认机制,MQ提供了三种策略:

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

        auto:自动ack,由spring运用AOP机制监测代码是否出现异常,没有异常则返回ack;抛出异常则返回nack

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

一般情况下,我们使用auto就能满足需求


auto模式开启:

        修改消费者的application.yml配置文件

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: auto # 开启spring管理

none模式开启:

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: none # 关闭ack

消息可靠性 -- 消费者失败重试机制

当消费者出现异常后,消息会requeue(重入队)到消息队列中,进行失败重试,但如果消费者依旧是不可用状态,name消息就会一直requeue,导致MQ消息负载,而spring为我们提供了retry机制(本地重试机制)

retry机制:

        当消息异常后,会在本地按照我们的自定义策略进行重试,这样可以避免MQ的消息负载

开启retry,修改消费者的application.yml文件

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

当超过了重试此时spring默认会抛出异常,同时将消息丢弃,


而spring也提供了三种重试失败策略供我们定制, MessageRecovery接口的三种实现:

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

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

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


比较实用的是RepublishMessageRecoverer自定义处理,我们可以定义一个专门处理这种问题消息的交换机,统一进行人工处理

@Configuration
public class ErrorMessageConfig {

    //定义专门处理的"error"交换机
    @Bean
    public DirectExchange errorMessageExchange(){
        return new DirectExchange("error.direct");
    }
    
    //定义"error"队列
    @Bean
    public Queue errorQueue(){
        return new Queue("error.queue", true);
    }

    //绑定交换机与队列
    @Bean
    public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){
        return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error");
    }


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

消息可靠性 -- 实现总结

  • 开启生产者确认机制,确保生产者的消息能到达队列

  • 开启持久化功能,确保消息未消费前在队列中不会丢失

  • 开启消费者确认机制为auto,由spring确认消息处理成功后完成ack

  • 开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理

死信交换机

当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter)

        消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false

        消息是一个过期消息,超时无人消费

        要投递的队列消息满了,无法投递


如果这个包含死信的队列配置了dead-letter-exchange属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机(Dead Letter Exchange,简称DLX)。

rabbitMQ高级_第3张图片

如果队列绑定了死信交换机,那么死信消息就会被推送到死信交换机中

rabbitMQ高级_第4张图片

如果死信交换机绑定了队列,那么死信消息就会进入到死信队列中

rabbitMQ高级_第5张图片

另外,队列将死信投递给死信交换机时,必须知道两个信息:

死信交换机名称

死信交换机与死信队列绑定的RoutingKey

这样才能确保投递的消息能到达死信交换机,并且正确的路由到死信队列。


上面讲到的RepublishMessageRecoverer失败重试机制,我们可以为队列配置死信交换机与死信队列就可以不用配置"error"交换机,

死信交换机 -- 实现消息延迟推送

 TTL超时:

        上面讲到称为死信有个条件是" 消息是一个过期消息,超时无人消费 ", 我们可以运用这一点,设置TTL超时的时间,实现消息的延迟推送

具体原理:

rabbitMQ高级_第6张图片

有两种方式设置超时时间:

        设置消息的超时时间:

@Test
public void testTTLMsg() {
    // 创建消息
    Message message = MessageBuilder
        .withBody("hello, ttl message".getBytes(StandardCharsets.UTF_8))
        .setExpiration("5000") //设置5000毫秒的超时
        .build();
    // 消息ID,需要封装到CorrelationData中
    CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    // 发送消息
    rabbitTemplate.convertAndSend("ttl.direct", "ttl", message, correlationData);
    log.debug("发送消息成功");
}

        设置队列的超时时间:

@Bean
public Queue ttlQueue(){
    return QueueBuilder.durable("ttl.queue") // 指定队列名称,并持久化
        .ttl(10000) // 设置队列的超时时间,10秒
        .deadLetterExchange("dl.ttl.direct") // 指定死信交换机
        .build();
}

当队列和消息都设置了超时时间的话,spring会以最短的那个时间为准

总结

        消息超时的两种设置方式:

                设置消息TTL属性,队列接收到消息后超过TTL时间的信息变为死信

                设置队列TTL属性,消息进入队列后超过TTL时间的信息变为死信

        如何实现消息的延迟推送:

                1) 给目标队列指定死信交换机

                2) 将消费者监听的队列绑定到死信交换机

                3) 设置消息/队列的TTL超时时间

死信交换机 -- 延迟队列插件 -- DelayExchange

利用TTL结合死信交换机,我们实现了消息发出后,消费者延迟收到消息的效果。这种消息模式就称为延迟队列(Delay Queue)模式

延迟队列的使用场景包括:

- 延迟发送短信
- 用户下单,如果用户在15 分钟内未支付,则自动取消
- 预约工作会议,20分钟后自动通知所有参会人员


因为延迟队列的需求非常多,所以RabbitMQ的官方也推出了一个插件,原生支持延迟队列效果。这个插件就是DelayExchange插件。Community Plugins — RabbitMQRabbitMQ插件库 Community Plugins — RabbitMQ

rabbitMQ高级_第7张图片

 使用方式可以参考官网地址:Scheduling Messages with RabbitMQ | RabbitMQ - Blog


DelayExchange原理

DelayExchange需要将一个交换机声明为delayed类型。当我们发送消息到delayExchange时,流程如下:

        - 接收消息
        - 判断消息是否具备x-delay属性
        - 如果有x-delay属性,说明是延迟消息,持久化到硬盘,读取x-delay值,作为延迟时间
        - 返回routing not found结果给消息发送者
        - x-delay时间到期后,重新投递消息到指定队列


使用DelayExchange

声明一个交换机,交换机的类型可以是任意类型,只需要设定delayed属性为true即可,然后声明队列与其绑定即可。

        基于注解:

rabbitMQ高级_第8张图片

         基于@Bean:rabbitMQ高级_第9张图片

 发送消息时,需要设置x-delay属性,给定超时时间

rabbitMQ高级_第10张图片

消息堆积 -- 惰性队列

消息堆积问题:

        当遇到消息的生产速度高于消息的处理速度的时候,消息就会在队列中堆积,引发消息堆积问题,解决消息堆积问题有两种方案

        使用work queue的队列模型,通过增加消费者来提高消息处理的速度

rabbitMQ高级_第11张图片

         提高队列的存储容量,而消息队列默认是将数据存储到内存中的,这样显然不满足需求,所以我们要使用RabbitMQ的3.6.0版本开始,增加的Lazy Queues的概念,也就是惰性队列


Lazy Queues(惰性队列):

        - 接收到消息后直接存入磁盘而非内存
        - 消费者要消费消息时才会从磁盘中读取并加载到内存
        - 支持数百万条的消息存储

使用惰性队列就能极大的提高消息队列的容量,不过这时候,消费消息的时候要从磁盘读取,会损耗一定的性能


开启Lazy Queues:

        设置惰性队列,有两种方式,可以在程序运行时,动态的将普通队列改为惰性队列,也可以在定义队列的时候直接设置为惰性队列

修改运行中的队列:

rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues  

命令解读:

- rabbitmqctl :RabbitMQ的命令行工具
- set_policy :添加一个策略
- Lazy :策略名称,可以自定义
- "^lazy-queue$" :用正则表达式匹配队列的名字
- '{"queue-mode":"lazy"}':设置队列模式为lazy模式
- --apply-to queues  :策略的作用对象,是所有满足正则表达式的队列

创建惰性队列:

        基于注解:

                rabbitMQ高级_第12张图片

         基于@Bean:

                rabbitMQ高级_第13张图片


总结

消息堆积问题的解决方案?

        - 队列上绑定多个消费者,提高消费速度
        - 使用惰性队列,可以再mq中保存更多消息        

惰性队列的优点有哪些?

        - 基于磁盘存储,消息上限高
        - 没有间歇性的page-out,性能比较稳定

惰性队列的缺点有哪些?

        - 基于磁盘存储,消息时效性会降低
        - 性能受限于磁盘的IO

MQ集群分类

RabbitMQ的是基于Erlang语言编写,而Erlang又是一个面向并发的语言,天然支持集群模式。RabbitMQ的集群有两种模式:

普通集群:是一种分布式集群,将队列分散到集群的各个节点,从而提高整个集群的并发能力。

镜像集群:是一种主从集群,普通集群的基础上,添加了主从备份功能,提高集群的数据可用性。

镜像集群虽然支持主从,但主从同步并不是强一致的,某些情况下可能有数据丢失的风险。因此在RabbitMQ的3.8版本以后,推出了新的功能:仲裁队列来代替镜像集群,底层采用Raft协议确保主从的数据一致性。

MQ集群 -- 普通集群

普通集群,或者叫标准集群(classic cluster),具备下列特征:

        - 会在集群的各个节点间共享部分数据,包括:交换机、队列元信息。不包含队列中的消息。
        - 当访问集群某节点时,如果队列不在该节点,会从数据所在节点传递到当前节点并返回
        - 缺点是队列所在节点宕机,队列中的消息就会丢失

rabbitMQ高级_第14张图片

MQ集群 -- 镜像集群

镜像集群:本质是主从模式,具备下面的特征:

        - 交换机、队列、队列中的消息会在各个mq的镜像节点之间同步备份。
        - 创建队列的节点被称为该队列的主节点,备份到的其它节点叫做该队列的镜像节点。
        - 一个队列的主节点可能是另一个队列的镜像节点
        - 所有操作都是主节点完成,然后同步给镜像节点
        - 主宕机后,镜像节点会替代成新的主节点

rabbitMQ高级_第15张图片

MQ集群 -- 仲裁集群

仲裁队列:仲裁队列是3.8版本以后才有的新功能,用来替代镜像队列,具备下列特征:

        - 与镜像队列一样,都是主从模式,支持主从数据同步
        - 使用非常简单,没有复杂的配置
        - 主从同步基于Raft协议Raft协议详解,强一致

你可能感兴趣的:(rabbitMQ高级,rabbitmq,分布式,java)