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上查看

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章