RabbitMQ

一.初识MQ

1.消息的发送流程

RabbitMQ包括生产者,交换机,队列,消费者。

队列与交换机相绑定,消费者订阅队列。

生产者通过交换机名称发送消息到指定交换机。

然后由交换机将消息路由到指定队列,最后由消费者消费队列中的信息。

实现了消息的异步接收。

2.虚拟主机

通过虚拟主机将不同应用,不同功能之间的队列与交换机隔离起来。

能有效避免以下问题:消息误发,命名重复。也可以实现权限控制。

3.WorkQueue任务模型

WorkQueue任务模型支持多个消费者绑定同一个队列,大大提高了消息的处理速度

4.交换机类型

Fanout:广播,将消息发送到与交换机绑定的队列

Direct:订阅,基于路由key发送消息给订阅了该交换机的队列

Topic:通配符订阅,和订阅类似,支持路由key使用通配符

在通配符订阅中,*代表任意个词,.代表一个词

5.消息转换器

RabbitMQ默认的消息转换器由Java编写。

因为RabbitMQ传输的是二进制数据,所以需要将消息对象序列化为二进制数据。

然后由消费者将二进制数据反序列化为消息对象。

这样导致默认的消息转换器存在以下缺点。

跨语言性极差,消息庞大且难懂。

所以我们需要自定义消息转换器

二.MQ进阶

1.消息丢失的几种情况

<1>发送消息时丢失

生产者没有连接到MQ

生产者找不到交换机

交换机没有路由消息到相关队列

消息到达MQ时,网络异常

<2>MQ接收到消息后丢失

消息到达队列中,尚未消费,MQ便宕机

<3>消费者处理消息时丢失

消费者接收到消息,未来得及处理,服务宕机

消费者处理消息时抛异常

2.发送者的可靠性

<1>生产者重试机制

当生产者连接MQ超时时,进行重新连接。主要针对网络问题

这种连接是阻塞式的,在高性能业务中往往不会使用。

即便使用这种机制,也要合理设置重试时间以及尝试次数,或者采用异步线程。

复制代码
spring:
  rabbitmq:
    connection-timeout: 1s # 设置MQ的连接超时时间
    template:
      retry:
        enabled: true # 开启超时重试机制
        initial-interval: 1000ms # 失败后的初始等待时间
        multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
        max-attempts: 3 # 最大重试次数
<2>生产者确认机制

主要针对消息发送到mq后消息丢失的问题。

包括Publisher Confim机制和Publisher return 机制

当消息到达MQ,但是交换机路由消息失败时,会返回异常信息和ack

当临时消息到达交换机并路由到队列时,会返回ack

当持久消息到达交换机并路由到队列,并完成持久化时,会返回ack

其他情况均返回nack

<<1开启生产者确认机制
复制代码
spring:
  rabbitmq:
    publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
    publisher-returns: true # 开启publisher return机制

none:关闭生产者确认机制

simple:同步阻塞等待MQ回执

correlated:异步回调等待MQ回执

异步回调等待MQ回执:

生产者发送消息到交换机后,直接执行之后的操作,等到mq确认完返回结果给生产者然后回调执行下面的内容。

<<2定义ReturnCallback

ReturnCallback是失败时返回回调,每个RabbitTemplate只能配置一个ReturnCallback,因此我们可以在配置类中统一设置。

复制代码
@Slf4j
@AllArgsConstructor
@Configuration
public class MqConfig {
    private final RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init(){
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            @Override
            public void returnedMessage(ReturnedMessage returned) {
                log.error("触发return callback,");
                log.debug("exchange: {}", returned.getExchange());
                log.debug("routingKey: {}", returned.getRoutingKey());
                log.debug("message: {}", returned.getMessage());
                log.debug("replyCode: {}", returned.getReplyCode());
                log.debug("replyText: {}", returned.getReplyText());
            }
        });
    }
}
<<3定义ConfirmCallback

确认回调,因为每个消息确认之后回调的逻辑都不相同,所以每个消息都应该由一个专门的ConfirmCallback,我们通过讲ConfirmCallback当作方法参数来实现这种逻辑。

复制代码
@Test
void testPublisherConfirm() {
    // 1.创建CorrelationData
    CorrelationData cd = new CorrelationData();
    // 2.给Future添加ConfirmCallback
    cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
        @Override
        public void onFailure(Throwable ex) {
            // 2.1.Future发生异常时的处理逻辑,基本不会触发
            log.error("send message fail", ex);
        }
        @Override
        public void onSuccess(CorrelationData.Confirm result) {
            // 2.2.Future接收到回执的处理逻辑,参数中的result就是回执内容
            if(result.isAck()){ // result.isAck(),boolean类型,true代表ack回执,false 代表 nack回执
                log.debug("发送消息成功,收到 ack!");
            }else{ // result.getReason(),String类型,返回nack时的异常描述
                log.error("发送消息失败,收到 nack, reason : {}", result.getReason());
            }
        }
    });
    // 3.发送消息
    rabbitTemplate.convertAndSend("hmall.direct", "q", "hello", cd);
}
<<4总结

为了解决生产者发送消息时丢失的问题,我们有两种机制,一中是生产者重试机制,一种是生产者确认机制。

通常采用第二种方法,因为第一种无法适应高性能业务。

生产者确认机制主要通过Pulisher Confirm和Publisher Return 。

我们采用异步回调等待MQ回执的方法实现生产者确认机制。

当消息发送失败时,会将消息返回给mq,并返回异常信息,我们通过重写ReturnCallback返回回调实现。

当消息发送到MQ时,会根据结果返回ack和nack然后进行ComfirmCallback回调。

我们将ComfirmCallback存到方法参数中,因为每个消息的回调的具体逻辑并不一致

3.MQ的可靠性

<1>数据持久化

交换机持久化 队列持久化 消息持久化

<2>LazyQueue

Queue接收到消息之后直接将消息存到磁盘中,只有消费者需要消费消息时,才将消息加载到内存中(懒加载,例如微信聊天)

为什么要采用懒加载:

当MQ中接收消息的速度远远大于消费者处理消息的宿舍时,MQ很快就会达到内存上线,然后将内存存到磁盘中(PageOut),会消耗大量时间,而且在这个过程中不会再接收生产者发送的消息,就会出现消息丢失。

4.消费者的可靠性

<1>消费者确认机制

当mq发送到消息给消费者之后,并不能确保消费者一定正确处理了该消息,所以需要直到消费者的处理状态,决定是否重新发送消息给消费者。Spring AMQP提供了消费者确认机制。

ack 成功处理消息,mq删除消息

nack 消息处理失败,mq重新添加消息到队列中

reject 消息处理失败并拒绝该消息,mq删除消息

<2>失败重试机制

当消息一直处理失败,一直返回nack,那么消息就会一直去添加到队列中,形成恶性循环,降低性能。

所以在消费者出现异常时现在本地重试,设置最大重试次数,本地重试成功则返回ack,否则返回nack或者reject,默认返回reject

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

这个策略是由MessageRecovery接口来定义的

,它有3个不同实现:

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

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

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

一般我们采用RepublishMessageRecover

失败后将消息投递到一个指定的,专门存放异常消息的队列,后续由人工集中处理。

复制代码
@Configuration
@ConditionalOnProperty(name = "spring.rabbitmq.listener.simple.retry.enabled", havingValue = "true")
public class ErrorMessageConfig {
    @Bean
    public DirectExchange errorMessageExchange(){
        return new DirectExchange("error.direct");
    }
    @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");
    }

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

5.延迟消息

某些消息不能被正常消费,就会成为死信。

主要有以下几种情况:

消费者reject消息,消息无法正常回到队列。

消息TTL超时。

队列达到最大内存限制,消息无法被正常接收。

延迟消息的实现主要通过以下方式:

设置一个队列,该队列不存在消费者,当消息TTL超时时就会进入死信交换机,然后由死信交换机通过路由key发送消息到指定队列。

但是这种方式有以下缺点:

RabbitMQ采用队首检查的方式检查过期消息,如果过期消息不位于队首,就没有办法被立即检测到,导致延迟时间不一定准确。

所以我们通过DelayExchange插件实现延迟消息发送。

开启方式:

一.基于注解方式 dalayed = "true"

复制代码
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "delay.queue", durable = "true"),
        exchange = @Exchange(name = "delay.direct", delayed = "true"),
        key = "delay"
))
public void listenDelayMessage(String msg){
    log.info("接收到delay.queue的延迟消息:{}", msg);
}

二.基于Bean

复制代码
@Bean
public DirectExchange delayExchange(){
    return ExchangeBuilder
            .directExchange("delay.direct")
            .delayed()
            .durable(true)
            .build();
}
相关推荐
西凉的悲伤1 小时前
redis-windows 安装 redis 到 windows 电脑
java·windows·redis·redis-windows
starsky762381 小时前
NIO与BIO的区别
java·服务器·nio
满怀冰雪1 小时前
第14篇-队列与单调队列-解决窗口最值问题的关键结构
java·算法
Mahir082 小时前
ConcurrentHashMap 底层原理深度解密:从分段锁到 CAS + 红黑树的演进全解
java·面试·concurhashmap
阿维的博客日记2 小时前
那用到动态代理,关键的特征又是什么呢
java·动态代理
都说名字长不会被发现2 小时前
Spring Boot Starter 中间件账号密码加密方案设计与实现
java·spring boot·后端·中间件
摇滚侠2 小时前
Maven 依赖范围
java·maven
AKA__Zas2 小时前
芝士算法(滑动窗口片 2.0)
java·算法·leetcode·学习方法
Zella折耳根4 小时前
复习篇-常用实用类
java