RabbitMq从使用到原理分析

目标:

从宏观上掌握RabbitMq这个消息中间件的基本原理。同时让阅读者掌握一些基本的使用方法。

大致原理介绍

为了实现解耦或者实现异步,将消息先发往独立于应用服务以外的一个中间服务(也就是mq)存储,其他服务在从这个中间服务获取消息,进行接下来的业务逻辑处理。整体流程如下:
在这里插入图片描述

消息中间件的作用

市场上包括各种中间件Kafka、RabbitMq、ActiveMq、RocketMq等。作用其实都是类似

  1. 解耦
  2. 流量削峰
  3. 异步通信
  4. 冗余、扩展、缓冲等

中间件的安装

建议参考地址

RabbitMq的整体架构分析

在这里插入图片描述
相关名词介绍:

  1. producer :生产者,可以理解为发送消息的一方
  2. consumer:消费者,可以理解为处理消息的一方
  3. broker:消息中间件服务,消息的中间方。安装mq服务的节点。

消息的流转过程

在这里插入图片描述
其他关键名词介绍:

  1. 交换机:可以理解为一个路由器,整个消息进度broker的第一个处理者。根据消息的不同,将消息放入不同的队列。
  2. 路由键:标志消息属于哪个队列(某些情况下该参数失效)
  3. 绑定: 将交换机和队列进行绑定,
  4. 队列:整个mq服务端(发送消息和消费消息的是客户端)用于存储消息的对象;

交换机类型

其实就是介绍交换机的常见模式。有一对一,也有一对多

1、fanout:可以理解为组播

凡是绑定在交换机下的队列都能收到消息。一个交换机会绑定很多个队列,这种情况下路由键会失效。

2、direct:完全根据路由key进行路由

在这里插入图片描述
可以看到路由键为warning的话,消息会被推到两个队列;路由key是info的话只会进入一个队列。这个就是direct类型的交互器的特征。

3、topic:绑定的key带有通配符

路由键带有通配符

4、head

实际使用介绍

实际使用介绍之前,我们先介绍下“连接”和“信道”的概念
在这里插入图片描述
生产者在和mq服务通信过程中是通过TCP协议,那个二者之间就会建立tcp连接,这种连接的建立通常是非常耗费时间,所以mq的设计者就使用了复用tcp连接的思路。那channel又是什么呢?他的中文翻译是信道,这个信道我们可以理解为完成一次逻辑通信的对象。比如我们可以是整个生产者服务和mq之间只有一条TCP链路,但是生产者可以是多线程的,多线程各自维护了一个和Mq服务通信的信道,也就是这里的channl ,chanel是一条逻辑上的通信链路。Connection是一条物理上的通信链路。
那为什么不直接使用Connection呢?主要是考虑各个线程之间的数据隔离
是不是无论多少个信道都可以共用一个Connection呢?不是,当信道数量越来越多的时候,一个Tcp连接可能不够用,我们应该适当的增加物理连接的数量。

最简单使用

默认交换机的使用

# 配置类
@Configuration
//@ConditionalOnProperty(prefix = SystemProperties.PREFIX, name = "openRabbitMq", havingValue = "true", matchIfMissing = true)
public class RabbitMqConfiguration {

    @Bean
    CommonConsumer commonConsumer(){
        return new CommonConsumer(); //申明一个默认消费者,
    }
     @Bean //定义一个普通队列,并没有给整个队列绑定交换机哦!
    public Queue commonQueue() {
        return new Queue(QueueEnum.COMMON_QUEUE.getQueueName());
    }
    @Bean
    public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(CachingConnectionFactory connectionFactory, MessageConverter messageConverter) {
        connectionFactory.setPublisherConfirms(true);
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        factory.setConnectionFactory(connectionFactory);
        factory.setMessageConverter(messageConverter);
        return factory;
    }

    @Bean
    public MessageConverter messageConverter() {//申明对象序列化类
        return new ContentTypeDelegatingMessageConverter(new Jackson2JsonMessageConverter());
    }

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory, MessageConverter messageConverter) {
        RabbitTemplate template = new RabbitTemplate(connectionFactory);
        template.setMessageConverter(messageConverter);
        template.setMandatory(true);
        return template;
    }
}

## 生产者
package com.defire.provider;

// 实现了 InitializingBean的对象在bean初始化时会调用afterPropertiesSet方法。
//ConfirmCallback & 和ReturnCallback 是为了实现消息确认机制,保证整个中间件的高可用。我们后文还会深入探讨
@Component
public class CommonProvider implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback, InitializingBean {

    static Logger logger = LoggerFactory.getLogger(CommonProvider.class);

    protected RabbitTemplate rabbitTemplate;
    public CommonProvider(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    /**
     * 发送消息
     *
     * @param messageContent
     */
    public void sendMessage(MessageContent messageContent, QueueEnum queueEnum) {
        if (messageContent != null ) {
            messageContent.setExchange(queueEnum.getExchange());//当使用默认队列的时候,交换机的名字是空
            messageContent.setQueueName(queueEnum.getQueueName());
            messageContent.setRouteKey(queueEnum.getRouteKey());

            MyCorrelationData correlationData = new MyCorrelationData(messageContent.getMessageId(), messageContent);
            correlationData.setExchange(queueEnum.getExchange());
            correlationData.setRoutingKey(queueEnum.getRouteKey());
            // 执行发送消息到指定队列
            rabbitTemplate.convertAndSend(queueEnum.getExchange(), queueEnum.getRouteKey(), messageContent, correlationData);
            logger.debug("CommonProvider新增消息内容:{}", JSON.toJSONString(messageContent));
        } else {
            logger.warn("消息内容为空或未开启队列!!!!!");
        }
    }

   
   
    /**
     * 用于实现消息发送到RabbitMQ交换器后接收ack回调,
     * 如果消息已经到到中间件,则会回调该方法。该方法在afterPropertiesSet中已经配置到rabbitTemplate对象上。
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            logger.debug("CommonProvider消息到达exchange成功,{}", correlationData == null ? cause : correlationData);
        } else {
            logger.debug("CommonProvider消息到达exchange失败,{}", correlationData == null ? cause : correlationData);
        }
    }

    /**
     * 用于实现消息发送到RabbitMQ交换器,但无相应队列与交换器绑定时的回调。
     * 如果消息已经到到中间件,但是中间件没有知道到对应的队列,则会回调该方法。
     * 该方法在afterPropertiesSet中已经配置到rabbitTemplate对象上。
     * @param message
     * @param replyCode
     * @param replyText
     * @param exchange
     * @param routingKey
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        if (!QueueEnum.MESSAGE_DELAY_QUEUE.getExchange().equals(exchange)) {
            logger.error("CommonProvider发送失败,replyCode:{}, replyText:{},exchange:{},routingKey:{},消息体:{}",
                    replyCode, replyText, exchange, exchange, routingKey, JSON.toJSONString(message));
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        this.rabbitTemplate.setMandatory(true);
        this.rabbitTemplate.setConfirmCallback(this);
        this.rabbitTemplate.setReturnCallback(this);
    }
}

上文简单的介绍了仅仅申明一个队列,未申明交互机,未进行绑定的默认情况。
如果使用默认交换机,则消息一定会被投递到和路由键一致的队列中。下面介绍其他类型的用法

其他特殊用法(两种实现延迟队列的方式)

所谓延迟队列,就是生产者将消息发送到队列后,队列不是立即消费,而是等待一段时间后才开始消费。常见的使用场景是交易系统中下单超时未支付的订单需要让其失效。为啥实现延时队列有两种方式呢?又是哪两种呢?
第一种是TTL队列+死信队列,第二种就是叫延迟队列。为啥有了第二种还需要第一种,我猜想是因为一开始并没有第二种,当延迟队列的需求确实很多了,官方才提供了延迟队列的插件。默认的rabbitmq是没有延迟队列的。需要为其单独安装插件。
上文刚刚提到了TTL队列,

什么是ttl呢?

其全称是time to live ,也就是队列里面的数据存在生存周期,如果生存周期内数据未被消费掉,那么,消息将被自动删除,当然如果ttl队列关联死信队列,则可以将ttl队列的消息转移至死信队列。

什么又是死信队列?

死信队列专门用于作为其他队列的协助队列,当主队列的消息不能被正常消费,或者主队列的消息过期了,则消息自动转到死信队列。

如何实现第一种延迟队列呢?设置两个队列,队列1(TTL队列)先收到生产者发来的消息,然而并没有消费者会消费队列1的消息,直到队列1消息过期,消息被转移到队列1关联的队列2(死信队列)而这个死信队列是指定了消费者的,所以消息一旦转移到死信队列,则立即被消费。如下图
在这里插入图片描述
代码上如何实现?

    /**
     * TTL交换机配置
     */
    @Bean
    DirectExchange ttlDirectExchange() {
        return (DirectExchange) ExchangeBuilder
                .directExchange(QueueEnum.MESSAGE_TTL_QUEUE.getExchange())
                .durable(true)
                .build();
    }
     /**
     * 定义TTL队列
     * 注意这个ttl队列申明的时候指定了死信交换机
     */
    @Bean
    Queue ttlQueue() {
        return QueueBuilder
                .durable(QueueEnum.MESSAGE_TTL_QUEUE.getQueueName())
                // 配置到期后转发的交换
                .withArgument("x-dead-letter-exchange", QueueEnum.MESSAGE_DEAD_QUEUE.getExchange())//注意这里绑定了死信交换机
                // 配置到期后转发的路由键
                .withArgument("x-dead-letter-routing-key", QueueEnum.MESSAGE_DEAD_QUEUE.getRouteKey())
                //注意这里绑定了死信队列
                .build();
    }
    // 死信交换机和死信队列
     /**
     * 死信消息交换机配置
     */
    @Bean
    DirectExchange deadDirectExchange() {
        return (DirectExchange) ExchangeBuilder
                .directExchange(QueueEnum.MESSAGE_DEAD_QUEUE.getExchange())
                .durable(true)
                .build();
    }
    /**
     * 定义死信队列
     */
    @Bean
    public Queue deadQueue() {
        return new Queue(QueueEnum.MESSAGE_DEAD_QUEUE.getQueueName());
    }
     /**
     * 死信队列和死信交换机的绑定-routekey
     * @param deadDirectExchange 消息中心交换配置
     * @param deadQueue  消息中心队列
     */
    @Bean
    Binding messageBinding(DirectExchange deadDirectExchange, Queue deadQueue) {
        return BindingBuilder
                .bind(deadQueue)
                .to(deadDirectExchange)
                .with(QueueEnum.MESSAGE_DEAD_QUEUE.getRouteKey());
    }
    /**
     * ttl队列和ttl交换机的绑定-routekey
     * @param ttlQueue
     * @param ttlDirectExchange
     */
    @Bean
    public Binding messageTtlBinding(Queue ttlQueue, DirectExchange ttlDirectExchange) {
        return BindingBuilder
                .bind(ttlQueue)
                .to(ttlDirectExchange)
                .with(QueueEnum.MESSAGE_TTL_QUEUE.getRouteKey());
    }

上文提到的都是如何生产消息,还没有提到如何消费消息。

 @RabbitListener(queues = QueueName.COMMON_QUEUE_NAME)//指定队列名字即可,如果是延迟队列,这里应该指定死信队列的名字
    public void handler(MessageContent messageContent, Channel channel, Message message) throws IOException {
        log.debug("BaseConsumer,消息内容:{}", JSON.toJSONString(messageContent));
        if (messageContent != null) {
            //做业务逻辑
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);//消费完后回复mq,mq则会从待确认队列中删除这个消息。如此来保证整体的可靠性。
            log.debug("BaseConsumer,消息内容:{}", JSON.toJSONString(messageContent));
        }
    }

以上我们分析完了第一种延迟队列的实现,现在我们看看第二种,第二种就更简单了。

    /**
     * 定义延迟队列
     * @return
     */
    @Bean
    public Queue delayQueue(){
        return new Queue(QueueEnum.MESSAGE_DELAY_QUEUE.getQueueName());
    }
     /**
     * 延迟消息交换机配置
     * @return
     */
    @Bean
    CustomExchange delayExchange(){
        Map<String,Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");
        return new CustomExchange(QueueEnum.MESSAGE_DELAY_QUEUE.getExchange(), "x-delayed-message", true, false, args);
    }
    //绑定
    @Bean
    public Binding delayBinding(Queue delayQueue, CustomExchange delayExchange) {
        return  BindingBuilder.bind(delayQueue).to(delayExchange).with(QueueEnum.MESSAGE_DELAY_QUEUE.getRouteKey()).noargs();
    }

至此延迟队列申明就完成,接下来的消费,就和普通队列完全一致了。同时我们对于延迟队列这种特殊队列的介绍也暂时完成,为此我们引入了TTL队列,死信队列。其实这俩队列都是可以单独使用的,并不是完全为了延迟队列而生。

作为一个中间件,我们总是要充分考虑其可用性,可靠性。那么整个过程中,是如何保证生产的消息一定会被消费呢?
其实rabbitmq提供了事务的方式和确认机制,两种方式来保证消费的可靠性。第一个由于性能太低我们就不介绍了。我们主要介绍确认机制。确认机制其实是包括好几种的,第一种是同步确认机制,第二种是异步确认机制。

首先是生产者,发送出去的消息会有一个回调通知,通知生产者消息是否被mq接收。

  /**
     * 用于实现消息发送到RabbitMQ交换器后接收ack回调
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            logger.debug("CommonProvider消息到达exchange成功,{}", correlationData == null ? cause : correlationData);
        } else {
            logger.debug("CommonProvider消息到达exchange失败,{}", correlationData == null ? cause : correlationData);
        }
    }

    /**
     * 用于实现消息发送到RabbitMQ交换器,但无相应队列与交换器绑定时的回调。
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        if (!QueueEnum.MESSAGE_DELAY_QUEUE.getExchange().equals(exchange)) {
            logger.error("CommonProvider发送失败,replyCode:{}, replyText:{},exchange:{},routingKey:{},消息体:{}",
                    replyCode, replyText, exchange, exchange, routingKey, JSON.toJSONString(message));
        }
    }
    @Override
    public void afterPropertiesSet() throws Exception {
        this.rabbitTemplate.setMandatory(true);
        this.rabbitTemplate.setConfirmCallback(this); //配置回调
        this.rabbitTemplate.setReturnCallback(this);//配置回调
    }

现在生产者已经放心了,自己发出去的消息有保证了。那么mq和消费者之间有哪些操作呢?

@RabbitListener(queues = QueueName.COMMON_QUEUE_NAME)
    public void handler(MessageContent messageContent, Channel channel, Message message) throws IOException {
        log.debug("BaseConsumer,消息内容:{}", JSON.toJSONString(messageContent));
        if (messageContent != null) {
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);//通知mq,消费者已经拿到消息。
            log.debug("BaseConsumer,消息内容:{}", JSON.toJSONString(messageContent));
        }
    }

channel.basicAck 通知mq,消费者已经拿到消息。

当然我们上面提到的仅是一种确认机制,能辅助三方之间沟通消息的发送,接收,消费状态。要提高整个系统的高可用,还得考虑很多其他方面。比如mq本身需要满足高可用(集群方式),还有消息&队列&交换机等实例在mq中要考虑将其持久化。这里就不深入讨论。
本文涉及到的代码可以在git上查看

猜你喜欢

转载自blog.csdn.net/ygy982883422/article/details/106974766
今日推荐