rabbitMQ(高性能的异步通信组件)

源码:https://github.com/njnuqy/rabbitMQDemo

同步调用

优点:时效性强,等待到结果后才返回
缺点:

  1. 扩展性差
  2. 性能下降
  3. 级联失败

异步调用

异步调用方式其实就是基于消息通知的方式,一般包含三个角色:

  1. 消息发送者:投递消息的人,就是原来的调用方
  2. 消息代理:管理,暂存,转发消息,你可以把他理解为微信服务器
  3. 消息接受者:接受和处理消息的人,就是原来的服务提供方
    优势:
    1. 解除耦合,拓展性强
    2. 无需等待,性能好
    3. 故障隔离
    4. 缓存消息,流量削峰填谷
      缺点:
    5. 不能立即得到调用结果,时效性差
    6. 不确定下游业务执行是否成功
    7. 业务安全依赖于Broker的可靠性

MQ技术选型

MQ(Message Queue),中文是消息队列,字面来看就是存放消息的队列。也就是异步调用中的Broker

方面RabbitMQActiveMQRocketMQKafka
公司/社区RabbitApache阿里Apache
开发语言ErlangJavaJavaScala&Java
协议支持AMQP,XMPP.SMTP.STOMPOpenWire,STOMP,REST,XMPP,AMQP自定义协议自定义协议
可用性一般
单击吞吐量一般非常高
消息延迟微妙级毫秒级毫秒级毫秒以内
消息可靠性一般一般

基本介绍

RabbitMQ的整体架构及核心概念:

  1. publisher:消息发送者
  2. consumer:消息的消费者
  3. queue:队列,存储消息
  4. exchange:交换机,负责路由消息
  5. virtual-host:虚拟主机,起到数据隔离的作用

快速入门

AMQP(Advanced Message Queuing Protocol),是用于在应用程序之间传递业务消息的开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求
Spring AMQP是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中spring-amqp是基础抽象,spring-rabbit是底层的默认实现

work模型

消费者消息推送限制

默认情况下,RabbitMQ的会将消息依次轮询问投递给绑定在队列上的每一个消费者。但这并没有考虑到消费者是否已经处理完消息,可能出现消息堆积
因此我们需要修改配置文件,设置preFetch为1,确保同一时刻最多投递给消费者一条消息:

work模型的使用

  1. 多个消费者绑定到一个队列,可以加快消息处理速度
  2. 同一条消息只会被一个消费者处理
  3. 通过设置prefetch来控制消费者预取的消息数量,处理完一条消息再处理下一条,实现能者多劳

f交换机

真正生产环境都会经过exchange来发送消息,而不是直接发送到队列,交换机的类型有以下三种

  1. Fanout:广播
  2. Direct:定向
  3. Topic:话题

Fanout交换机

Fanout exchange会将接收到的消息广播到每一个跟其绑定的queue,所以也叫广播模式
在这里插入图片描述

Direct交换机

Direct Exchange会将接收到的消息根据规则路由到指定的Queue,因此成为定向路由

  1. 每一个Queue都与Exchange设置一个BindingKey
  2. 发布者发布消息时,指定消息的RoutingKey
  3. Exchange将消息路由到BingingKey与消息RoutingKey一致的队列

Topic交换机

TopicExchange与DirectExchange类似,区别在于routingKey可以是多个单词的列表,并且以.分割
Queue与Exchange指定BingingKey时可以使用通配符:

  1. #代表0个或多个单词
  2. *代指一个单词

声明队列交换机

S平日那个AMQP提供了几个类,用来声明队列,交换机及其绑定关系:

  1. queue:用于声明队列,可以用工厂类的QueueBuilder构建
  2. Exchange:用于声明交换机,可以用工厂类ExchangeBuilder构建
  3. Binding:用于声明队列和交换机的绑定关系,可以用工厂类的BingingBuilder构建

消息转换器

Spring的对消息对象的处理是由MessageConverter来处理的。而默认实现是SinpleMessageConverter吗,基于JDK的ObejctOutputStream完成序列化
存在下列问题

  1. JDK的序列化存在安全风险
  2. JDK序列化的消息太大
  3. JDK序列化的消息可读性差

消息可靠性问题

生产者可靠性

生产者重连

有的时候由于网络波动,可能会出现客户端连接MQ失败的情。通过配置我们可以开启失败后的重连机制

spring.rabbitmq.connection-timeout = 1s #设置MQ的链接超时时间
spring.rabbitmq.template.retry.enabled = true #开启超时重试机制
spring.rabbitmq.template.retry.initial-interval = 1000ms #失败后的初始等待时长
spring.rabbitmq.template.retry.multiplier = 1 #失败后下次的等待时长倍数
spring.rabbitmq.template.retry.max-attempts = 3 #最大重试次数


当网络不稳的时候,利用重试机制可以有效提高消息发送的成功率。不过SpringAMQP提供的重试机制是阻塞式的重试,也就是说多次重试等待的过程中,当前线程是被阻塞的,会影响业务性能
如果对业务性能有要求,建议禁用重试机制,如果一定要使用,请合理配置等待时长和重试次数。当然也可以考虑使用异步线程来执行发送消息的代码。

生产者确认

RabbitMQ有Publisher Confirm和Publisher Return两种确认机制。开启确认机制后,在MQ成功收到消息后会返回确认消息给生产者。返回的结果有以下几种情况。
在这里插入图片描述

  1. 消息投递到了MQ,但是路由失败。此时会通过PublisherReturn返回路由异常原因,然后返回ACK,告知投递成功。
  2. 临时消息投递到了MQ,并且入队成功,返回ACK,告知投递成功
  3. 持久消息投递到了MQ,并且入队完成持久化,返回ACK,告知投递成功。
  4. 其他情况都会返回NACK,告知投递失败。

SpringAMQP实现生产者确认

  1. 在Publisher这个微服务中添加配置
spring-rabbitmq-publisher-confirm-type = correlated
spring-rabbitmq-publisher-returns = true

这里的publisher-confirm-type有三种模式可选

  1. none:关闭confirm机制
  2. simple:同步阻塞等待MQ的回执消息
  3. correlated:MQ异步回调方式返回回执消息

每一个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目启动过程配置

@Configuration
@Slf4j
public class MQConfirmConfig implements ApplicationContextAware {

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        // 配置回调
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            @Override
            public void returnedMessage(ReturnedMessage returnedMessage) {
                log.debug("收到消息的return callback,exchange:{},key:{},msg:{},code:{},text:{}",
                        returnedMessage.getExchange(),returnedMessage.getRoutingKey(),returnedMessage.getMessage(),
                        returnedMessage.getReplyCode(),returnedMessage.getReplyText());
            }
        });
    }
}

如何处理生产者的确认消息

  1. 生产者确认需要额外的网络和系统资源开销,尽量不要使用
  2. 如果一定要使用,无需开启Publisher-Return机制,因为一般路由失败是自己业务问题
  3. 对于nack消息可以有限次数重试,依然失败则记录异常消息

MQ的可靠性

在默认情况下,RabbitMQ会将接收到的消息保存在内存中以降低消息收发的延迟。这样会导致两个问题

  1. 一旦MQ宕机,内存中的消息会丢失
  2. 内存空间有限,当消费者故障或处理过慢时,会导致消息积压,引发MQ阻塞

数据持久化

  1. 交换机持久化
  2. 队列持久化
  3. 消息持久化
@Test
    void testPageOut(){
        Message message = MessageBuilder.withBody("hello".getBytes(StandardCharsets.UTF_8))
                .setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();
        for (int i = 0; i < 1000000; i++) {
            rabbitTemplate.convertAndSend("simple.queue",message);
        }
    }

Lazy Queue

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

  1. 接收到消息后直接存入磁盘而非内存(内存只保留最近的消息,默认2048条)
  2. 消费者要消费消息时才会从磁盘读取并且加载到内存
  3. 支持数百万条的消息存储
    在3.12版本后,所有队列都是Lazy Queue模式,无法更改

要设置一个队列是惰性队列,只需要在声明队列时,指定x-queue-mode属性为lazy即可:

@Bean
public Queue lazyQueue(){
	return QueueBuilder
			.durable("lazy.queue")
			.lazy() //开启lazy模式
			.build();
}

RabbitMQ如何保证消息的可靠性

  1. 首先通过配置可以让交换机,队列,以及发送的消息都持久化。这样队列中的消息会持久化到磁盘,MQ重启消息依然存在
  2. RabbitMQ在3.6版本引入了LazyQueue,在3.12版本后,所有队列都是Lazy Queue模式。Lazy Queue会将所有消息都持久化
  3. 开启持久化和生产者确认时,RabbitMQ只有在消息持久化完成后才会给生产者返回ACK回执

消费者的可靠性

消费者确认机制

为了确认消费者是否成功处理消息,RabbitMQ提供了消费者确认机制(Consumer Acknowledgement)。当消费者处理消息结束后,应该像RabbitMQ发送一个回执,告知RabbitMQ自己消息处理中状态。回执有三种可选值:

  1. ack:成功处理消息,RabbitMQ从队列中删除消息
  2. nack:消息处理失败,RabbitMQ需要再次投递消息
  3. reject:消息处理失败并拒绝该消息,RabbitMQ从队列中删除该消息

SpringAMQP已经实现了消息确认功能。并允许我们通过配置文件选择ACK处理方式,有三种方式:

  1. none:不处理。即投递消息给消费者后立刻ack,消息会立刻从MQ删除。非常不安全,不建议使用
  2. manual:手动模式。需要自己在业务代码中调用api,返回ack或reject,存在业务入侵,但更灵活
  3. auto:自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack,当业务出现异常时,根据异常判断返回不同结果
    1. 如果是业务异常,会自动返回nack
    2. 如果是消息处理或校验异常,自动返回reject
spring-rabbitmq-listener.simple.acknowledge-mode = none

失败重试机制

当消费者出现异常后,消息会不断地requeue(重新入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力。
我们可以利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列:

spring.rabbitmq.listener.simple.retry.enabled = true
spring.rabbitmq.listener.simple.retry.initial-interval=1000ms
spring.rabbitmq.listener.simple.retry.multiplier=1
spring.rabbitmq.listener.simple.retry.max-attempts=3
spring.rabbitmq.listener.simple.retry.stateless=true

失败消息处理策略

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

  1. RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
  2. ImmediateRequeueMessageRecoverer:重试耗尽后,返回 nack,消息重新入队
  3. RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机

将失败处理策略改为RepublishMessageRecoverer:
4. 首先,定义接收失败消息的交换机,队列,及其绑定关系,此处略
5. 然后定义RepublisherMessageRecoverer

@Slf4j
@Configuration
@ConditionalOnProperty(prefix = "spring.rabbitmq.listener.simple.retry",name = "enabled",havingValue = "true")
public class ErrorConfiguration {
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange("error.exchange");
    }
    @Bean
    public Queue errorQueue(){
        return new Queue("error.queue");
    }

    @Bean
    public Binding errorBinding(Queue errorQueue,DirectExchange directExchange){
        return BindingBuilder.bind(errorQueue).to(directExchange).with("error");
    }

    @Bean
    public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate){
        log.debug("加载RepublishMessageRecoverer");
        return new RepublishMessageRecoverer(rabbitTemplate,"error.dirent","error");
    }
}

业务幂等性

幂等是一个数学概念,用函数表达式来描述是这样的:f(x)=f(f(x))。在程序开发中,则是指同一个业务,执行一次或多次对业务状态的影响是一致的
在这里插入图片描述

唯一消息id

方案一,是给每一条消息设置一个唯一id,利用id来区分是否是重复消息

  1. 每一条消息都生成一个唯一的id,与消息一起投递给消费者
  2. 消费者接收到消息后处理自己的业务,业务处理成功后将消息ID保存到数据库
  3. 如果下次又收到相同消息,去数据库查询判断是否存在,存在则为重复消息放弃处理
@Bean
    public MessageConverter jacksonMessage(){
    	// 1. 定义消息转换器
        Jackson2JsonMessageConverter jjmc = new Jackson2JsonMessageConverter();
        // 2. 配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
        jjmc.setCreateMessageIds(true);
        return jjmc;
    }

业务判断

方案二,是基于业务逻辑,基于业务本身做判断。以我们的业务为例:我们要在支付后修改订单状态为已支付,应该在修改订单状态前先查询订单状态,判断状态是否是未支付。只要未支付的订单才需要修改,其他状态不做处理:
在这里插入图片描述

如果交易服务消息处理失败,有没有什么兜底方案

我们可以在交易服务中设置定时任务,定期查询订单支付状态。这样即使MQ通知失败,还可以利用定时任务作为兜底方案,确保订单支付状态的最终一致性。

延迟消息

延迟消息:生产者发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间之后才收到消息
延迟任务:设置在一定时间后才执行的任务
在这里插入图片描述

死信交换机

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

  1. 消费者使用basic.reject或basic.nack声明消费失败,并且消息的request参数设置为false
  2. 消息是一个过期消息(达到了队列或消息本身设置的过期时间),超时无人消费
  3. 要投递的队列消息堆积满了,最早的消息可能成为死信。
    如果队列通过dead-letter-exchange属性指定了一个交换机,那么队列中的死信就会投递到这个交换机中。这个交换机成为死信交换机(Dead Letter Exchange简称DLX)
    在这里插入图片描述
@Test
    void testSendTTLMessage(){
        rabbitTemplate.convertAndSend("simple.queue","hi","hello",new MessagePostProcessor(){
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                message.getMessageProperties().setExpiration("1000");
                return message;
            }
        });
    }

延迟消息插件

RabbitMQ的官方也推出了一个插件,原生支持延迟消息功能。该插件的原理是设计了一种支持延迟消息功能的交换机,当消息投递到见换机后可以暂存一定时间,到期后再投递到队列
在这里插入图片描述

@RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "delay.queue",durable = "true"),
            exchange = @Exchange(value = "delay.direct",delayed = "true"),
            key = "hi"
    ))
    public void listenDelayQueue(String msg){
        System.out.println("接收到delay的消息");
    }
  • 28
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值