『假如我是面試官』RabbitMQ我會這樣問

Java旅途發表於2021-06-22

1. 為什麼你們公司選擇RabbitMQ作為訊息中介軟體

在訊息佇列選型時,我們調研了市場上比較常用ActiveMQ,RabbitMQ,RocketMQ,Kafka。

  1. RabbitMQ相對成熟穩定,這是我們選擇它最主要的原因。
  2. 社群比較活躍,有完善的資料可以參考。
  3. Rabbitmq的吞吐量可以達到萬級,完全滿足我們系統的要求。
  4. RabbitMQ是Erlang語言開發的,效能比較好。
  5. 有完善的視覺化介面,方便檢視。

2. 訊息佇列的優點和缺點有哪些

優點有:

  • 非同步處理 - 相比於傳統的序列、並行方式,提高了系統吞吐量。
  • 應用解耦 - 系統間通過訊息通訊,不用關心其他系統的處理。
  • 流量削鋒 - 可以通過訊息佇列長度控制請求量;可以緩解短時間內的高併發請求。

缺點有:

  • 系統可用性降低
  • 系統複雜度提高

3. RabbitMQ常用的工作模式有哪些

2.1 簡單模型

  • p:生成者
  • C:消費者
  • 紅色部分:quene,訊息佇列

2.2 工作模型

這種模式下一條訊息只能由一個消費者進行消費,預設情況下,每個消費者是輪詢消費的。

  • p:生成者
  • C1、C2:消費者
  • 紅色部分:quene,訊息佇列

2.3 釋出訂閱模型(fanout)

這種模型中生產者傳送的訊息所有消費者都可以消費。

  • p:生成者
  • X:交換機
  • C1、C2:消費者
  • 紅色部分:quene,訊息佇列

2.4 路由模型(routing)

這種模型消費者傳送的訊息,不同型別的訊息可以由不同的消費者去消費。

  • p:生成者
  • X:交換機,接收到生產者的訊息後將訊息投遞給與routing key完全匹配的佇列
  • C1、C2:消費者
  • 紅色部分:quene,訊息佇列

2.5 主題模型(topic)

這種模型和direct模型一樣,都是可以根據routing key將訊息路由到不同的佇列,只不過這種模型可以讓佇列繫結routing key 的時候使用萬用字元。這種型別的routing key都是由一個或多個單片語成,多個單詞之間用.分割。

萬用字元介紹:

*:只匹配一個單詞

#:匹配一個或多個單詞

4. 如何保證訊息不丟失(如何保證訊息的可靠性)

一條訊息從生產到消費經歷了三個階段,分別是生產者,MQ和消費者,對於RabbitMQ來說,訊息的傳遞還涉及到交換機。因此RabbitMQ出現訊息丟失的情況有四個

分別是

  1. 訊息生產者沒有成功將訊息傳送到MQ導致訊息丟失
  2. 交換機未路由到訊息佇列導致訊息丟失
  3. 訊息在MQ中時,MQ發生當機導致訊息丟失
  4. 消費者消費訊息時出現異常導致訊息丟失

針對上面提到的四種情況,分別進行處理

  1. amqp協議提供了事務機制,在投遞訊息時開啟事務,如果訊息投遞失敗,則回滾事務,很少有人去使用事務。除了事務之外,RabbitMQ還提供了生產者確認機制(publisher confirm)。生產者將通道設定成confirm(確認)模式,一旦通道進入confirm模式,所有在該通道上面釋出的訊息都會被指派一個唯一的ID(從1開始),一旦訊息被投遞到所有匹配的佇列之後,RabbitMQ就會傳送一個確認(Basic.Ack)給生產者(包含訊息的唯一ID),這就使得生產者知曉訊息已經正確到達了目的地了。
# 開啟生產者確認機制,
# 注意這裡確認的是是否到達交換機
spring.rabbitmq.publisher-confirm-type=correlated
@RestController
public class Producer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("send")
    public void sendMessage(){
        /**
         * 生產者確認訊息
         */
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                System.out.println(correlationData);
                System.out.println(ack);
                System.out.println(cause);
            }
        });
        rabbitTemplate.convertAndSend("s","error","這是一條錯誤日誌!!!");
    }
}
  1. 訊息從交換機未能匹配到佇列時將此條訊息返回給生產者
spring.rabbitmq.publisher-returns=true
@RestController
public class Producer {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("send")
    public void sendMessage(){
        /**
         * 訊息未達佇列時返回該條訊息
         */
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            @Override
            public void returnedMessage(ReturnedMessage returnedMessage) {
                System.out.println(returnedMessage);
            }
        });
        rabbitTemplate.convertAndSend("s","error","這是一條錯誤日誌!!!");
    }
}
  1. 訊息在交換機或佇列中發生丟失,我們只需要將交換機和佇列進行持久化。
/**
  * 定義一個持久化的topic交換機
  * durable 持久化
  * @return
 */
@Bean
public Exchange exchangeJavatrip(){
    return ExchangeBuilder.topicExchange(EXCHANGE).durable(true).build();
}

/**
 * 定義一個持久化的佇列
 * durable 持久化
 * @return
 */
@Bean
public Queue queueJavatrip(){
    return QueueBuilder.durable(QUEUE).build();
}
  1. 消費者開啟手動簽收模式,消費完成後進行ack確認。
spring.rabbitmq.listener.simple.acknowledge-mode=manual
@RabbitListener(queues = MqConfig.QUEUE)
public void receive(String body, Message message, Channel channel) throws Exception{
    long deliveryTag = message.getMessageProperties().getDeliveryTag();
    System.out.println(deliveryTag);
    // 系統業務邏輯判斷是否簽收
    if(deliveryTag % 2 == 0){
        channel.basicAck(deliveryTag,false);
    }else{
        // 第二個引數是否批量確認,第三個引數是否重新回佇列
        channel.basicNack(deliveryTag,false,true);
    }
}

5. 如何保證訊息不重複消費(如何保證訊息的冪等性)

訊息重複的原因有兩個:

  1. 生產時訊息重複

    由於生產者傳送訊息給MQ,在MQ確認的時候出現了網路波動,生產者沒有收到確認,實際上MQ已經接收到了訊息。這時候生產者就會重新傳送一遍這條訊息。

  2. 消費時訊息重複。

    消費者消費成功後,在給MQ確認的時候出現了網路波動,MQ沒有接收到確認,為了保證訊息被消費,MQ就會繼續給消費者投遞之前的訊息。這時候消費者就接收到了兩條一樣的訊息。

由於訊息重複是網路波動等原因造成的,無法避免,我們能做的的就是保證訊息的冪等性,以防業務重複處理。具體處理方案為:

讓每個訊息攜帶一個全域性的唯一ID,即可保證訊息的冪等性,具體消費過程為:

  1. 消費者獲取到訊息後先根據id去查詢redis/db是否存在該訊息。
  2. 如果不存在,則正常消費,消費完畢後寫入redis/db。
  3. 如果存在,則證明訊息被消費過,直接丟棄。
@RabbitListener(queues = MqConfig.QUEUE)
public void receive(Message message, Channel channel){

    String messageId = message.getMessageProperties().getMessageId();
    String body = new String(message.getBody());
    String redisId = redisTemplate.opsForValue().get(messageId)+"";
    // 如果redis中存有當前訊息的訊息id
    // 則證明消費過
    if(messageId.equals(redisId)){
        return;
    }
    redisTemplate.opsForValue().set(messageId, UUID.randomUUID());
}

6. 訊息大量堆積應該怎麼處理

訊息堆積的原因有兩個

  1. 網路故障,消費者無法正常消費
  2. 消費方消費後未進行ack確認

解決方案如下:

  1. 檢查並修復消費者故障,使其正常消費
  2. 編寫臨時程式將堆積的訊息傳送到容量更大的MQ叢集,增加消費者快速消費
  3. 堆積訊息消費完畢後,停止臨時程式,恢復正常消費

7. 死信是什麼?死信如何處理

當一條訊息在佇列中出現以下三種情況的時候,該訊息就會變成一條死信。

  • 訊息被拒絕(basic.reject / basic.nack),並且requeue = false
  • 訊息TTL過期
  • 佇列達到最大長度

當訊息在一個佇列中變成一個死信之後,如果配置了死信佇列,它將被重新publish到死信交換機,死信交換機將死信投遞到一個佇列上,這個佇列就是死信佇列。

一條訊息成為死信後,一般會通過死信佇列進行存庫,然後定時將庫中的死信進行重新投遞到訊息佇列上。

8. 如果我有一筆訂單,30分鐘未支付則關閉訂單,使用RabbitMQ如何來實現

RabbitMQ可以使用死信佇列來實現延時消費,使用者下單之後,將訂單資訊投遞到訊息佇列中,並且設定訊息過期時常為30分鐘。如果使用者支付則正常關閉訂單,如果使用者未支付,訊息達到過期時間,訊息會進入死信交換,由消費者進行消費死信佇列來關閉訂單。

9. RabbitMQ如何保證高可用

RabbitMQ有兩種叢集模式,分別是普通叢集和映象叢集,普通模式無法保證RabbitMQ的高可用。

普通叢集

假如有三個節點,rabbitmq1、rabbitmq2、rabbitmq3,訊息實際上只存在於其中一個節點,三個節點僅有相同的後設資料,即佇列的結構,當訊息進入rabbitmq2節點的queue後,consumer從rabbitmq1的節點進行消費,rabbitmq1和rabbitmq2會進行臨時通訊,從rabbitmq2中獲取訊息然後返回給consumer。

這種模式存在以下兩個問題:

  1. 當rabbitmq2當機後,訊息無法正常消費,沒有做到真正的高可用

  2. 實際資料還是在單個例項上,存在瓶頸問題

映象叢集

假如有三個節點,rabbitmq1、rabbitmq2、rabbitmq3,每個例項之間都可以相互通訊,每次生產者寫訊息到queue的時候,每個rabbitmq節點上都有queue的訊息資料和後設資料。這種模式使用於可靠性要求較高的場景。

點關注、不迷路

如果覺得文章不錯,歡迎關注、點贊、收藏,你們的支援是我創作的動力,感謝大家。

如果文章寫的有問題,請不要吝惜文筆,歡迎留言指出,我會及時核查修改。

如果你還想看到更多別的東西,可以微信搜尋「Java旅途」進行關注。回覆“手冊”領取Java面試手冊!

相關文章