rabbitmq

rabbit的三种发送订阅模式

消息从发送,到消费者接收,会经历多个过程:

其中的每一步都可能导致消息丢失,常见的丢失原因包括:

  • 发送时丢失:
  • 生产者发送的消息未送达exchange
  • 消息到达exchange后未到达queue
  • MQ宕机,queue将消息丢失
  • consumer接收到消息后未消费就宕机

针对这些问题,RabbitMQ分别给出了解决方案。也就是面试题常问的如何rabbitmq如何保证消息的可靠性?

  1. 生产者确认机制
  2. mq持久化
  3. 消费者确认机制
  4. 失败重试机制

生产者确认机制(解决生产者->交换机->队列)

步骤

1.修改生产者配置

java 复制代码
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.定义回调函数(每个RabbitTemplate只能配置一个ReturnCallback)

消息从交换机到队列的过程不通就会回调这个函数

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // 获取RabbitTemplate
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        // 设置ReturnCallback
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            // 投递失败,记录日志
            log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
                     replyCode, replyText, exchange, routingKey, message.toString());
            // 如果有业务需要,可以重发消息
        });
    }
}

3.定义ConfirmCallback 消息每次发送前都要写

java 复制代码
public void testSendMessage2SimpleQueue() throws InterruptedException {
    // 1.消息体
    String message = "hello, spring amqp!";
    // 2.全局唯一的消息ID,需要封装到CorrelationData中
    CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
    // 3.添加callback
    correlationData.getFuture().addCallback(
        result -> {
            if(result.isAck()){
                // 3.1.ack,消息成功
                log.debug("消息发送成功, ID:{}", correlationData.getId());
            }else{
                // 3.2.nack,消息失败
                log.error("消息发送失败, ID:{}, 原因{}",correlationData.getId(), result.getReason());
            }
        },
        ex -> log.error("消息发送异常, ID:{}, 原因{}",correlationData.getId(),ex.getMessage())
    );
    // 4.发送消息
    rabbitTemplate.convertAndSend("task.direct", "task", message, correlationData);

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

mq持久化(推荐惰性队列)

  • 交换机持久化

  • 队列持久化

  • 消息持久化

  • 交换机持久化

java 复制代码
@Bean
public DirectExchange simpleExchange(){
    // 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除
    return new DirectExchange("simple.direct", true, false);
}
  • 队列持久化
java 复制代码
@Bean
public Queue simpleQueue(){
    // 使用QueueBuilder构建队列,durable就是持久化的
    return QueueBuilder.durable("simple.queue").build();
}

消息持久化

发送时,可以设置消息的属性(MessageProperties),指定delivery-mode

但是这三种都不是很好的方式

惰性队列:从RabbitMQ的3.6.0版本开始,就增加了Lazy Queues的概念,也就是惰性队列。惰性队列的特征如下:

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

消费者确认机制

直接配置即可

java 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual # 关闭ac
  • none模式下,消息投递是不可靠的,可能丢失
  • auto模式类似事务机制,出现异常时返回nack,消息回滚到mq;没有异常,返回ack
  • manual:自己根据业务情况,判断什么时候该ack

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

手动ack

java 复制代码
    @RabbitListener(queues = "fanout.queue1")
public void onMessage(Message message, Channel channel) throws Exception {
    try {
        // 处理消息
        processMessage(message);
        // 手动确认消息已经被处理完成
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    } catch (Exception e) {
        // 发生异常时可以选择拒绝消息,重新放回队列或者进入死信队列等
        channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
    }
}

失败重试机制

1.生产者发送消息失败配置

默认是消息重新回到队列,但是如果一直失败,就会一直循环的失败回到队列再重试,频率太高太消耗资源了

可以本地重试也就是重写发送消息

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

经典面试题:如何保证消息不会重复消费呢?

1.消费端手动ACK确认机制

在消费端使用RabbitMQ提供的手动ACK确认机制,在消费者成功处理消息后,手动将消息从队列中删除。这样可以确保消息只会被处理一次,避免了重复消费的问题。

2.消费端去重保证机制

可以在消费端处理每条消息之前,通过分布式锁或者数据库唯一索引等方式,判断当前消息是否已经被处理过。如果已经处理过,则忽略该消息;否则正常处理,并将消息标记为已处理。

死信队列

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

  • 消费者使用basic.reject或 basic.nack声明消费失败,并且消息的requeue参数设置为false
  • 消息是一个过期消息,超时无人消费
  • 要投递的队列消息满了,无法投递

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

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

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

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


java 复制代码
// 声明普通的 simple.queue队列,并且为其指定死信交换机:dl.direct
@Bean
public Queue simpleQueue2(){
    return QueueBuilder.durable("simple.queue") // 指定队列名称,并持久化
        .deadLetterExchange("dl.direct") // 指定死信交换机
        .build();
}
// 声明死信交换机 dl.direct
@Bean
public DirectExchange dlExchange(){
    return new DirectExchange("dl.direct", true, false);
}
// 声明存储死信的队列 dl.queue
@Bean
public Queue dlQueue(){
    return new Queue("dl.queue", true);
}
// 将死信队列 与 死信交换机绑定
@Bean
public Binding dlBinding(){
    return BindingBuilder.bind(dlQueue()).to(dlExchange()).with("simple");
}

死信交换机的使用场景是什么?

  • 如果队列绑定了死信交换机,死信会投递到死信交换机;
  • 可以利用死信交换机收集所有消费者处理失败的消息(死信),交由人工处理,进一步提高消息队列的可靠性。

延迟队列

概念:延迟队列是一种消息中间件中的特殊队列,用于暂存需要延迟处理的消息,并在一定时间后重新发送到原来的队列或其他队列中进行处理
延迟消息 :是指具有延迟时间属性的消息,在发送时可以设置消息的延迟时间。这个延迟时间表示消息需要在多少时间后才能被消费者消费。

两者关系:延迟队列是实现延迟消息的一种方式
定时消息和延迟消息本质上是相同的。它们都根据消息设置的计时时间在固定时间向消费者传递消息。 -----rocketmq官网写的

允许消息在设置的时间内未被消费,如果到期,未消费发送到死信队列
模式一 :消息本身就可以设置过期时间,设置了过期时间的消息发送到一个没有消费者监听的队列,该队列绑定了死信交换机。当消息过期就会发送到死信交换机,然后再到死信队列。我们只需监听死信队列即可

上面这种方式不推荐。

官方推荐
模式二使用插件,

RabbitMQ有一个官方的插件社区,地址为:https://www.rabbitmq.com/community-plugins.html

其中包含各种各样的插件,包括我们要使用的DelayExchange插件:

大家可以去对应的GitHub页面下载3.8.9版本的插件,地址为https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/tag/3.8.9这个对应RabbitMQ的3.8.5以上版本

相关推荐
processflow流程图2 小时前
分布式kettle调度平台v6.4.0新功能介绍
分布式
全栈开发圈2 小时前
干货分享|分布式数据科学工具 Xorbits 的使用
分布式
运维&陈同学5 小时前
【zookeeper01】消息队列与微服务之zookeeper工作原理
运维·分布式·微服务·zookeeper·云原生·架构·消息队列
时差9535 小时前
Flink Standalone集群模式安装部署
大数据·分布式·flink·部署
菠萝咕噜肉i5 小时前
超详细:Redis分布式锁
数据库·redis·分布式·缓存·分布式锁
只因在人海中多看了你一眼8 小时前
分布式缓存 + 数据存储 + 消息队列知识体系
分布式·缓存
zhixingheyi_tian11 小时前
Spark 之 Aggregate
大数据·分布式·spark
求积分不加C12 小时前
-bash: ./kafka-topics.sh: No such file or directory--解决方案
分布式·kafka
nathan052912 小时前
javaer快速上手kafka
分布式·kafka
谭震鸿16 小时前
Zookeeper集群搭建Centos环境下
分布式·zookeeper·centos