RabbitMQ由淺入深入門全總結(二)

BWH_Steven 發表於 2021-06-17

寫在最前面

距離上一次發文章已經很久了,其實這段時間一直也沒有停筆,只不過在忙著找工作還有學校結課的事情,重新弄了一下部落格,後面也會陸陸續續會把文章最近更新出來~

  • 這篇文章有點長,就分了兩篇
  • PS:那個Github上Java知識問答的文章也沒有停筆,最近也會陸續更新

文章目錄:

6. 進階補充

6.1 過期時間設定(TTL)

過期時間(TTL)就是對訊息或者佇列設定一個時效,只有在時間範圍內才可以被被消費者接收穫取,超過過期時間後訊息將自動被刪除。

注:我們主要講訊息過期,在訊息過期的第一種方式中,順便也就會提到佇列過期的設定方式

  1. 通過佇列屬性設定,佇列中所有訊息都有相同的過期時間
  2. 對訊息進行單獨設定,每條訊息 TTL可以不同

兩種方法同時被使用時,以兩者過期時間 TTL 較小的那個數值為準。訊息在佇列的生存時間一旦超過設定的 TTL 值,就稱為 Dead Message 被投遞到死信佇列,消費者將無法再收到該訊息(死信佇列是我們下一點要講的)

6.1.1 應用於全部訊息的過期時間

  • 配置類
@Configuration
public class RabbitMqConfiguration {

    public static final String TOPIC_EXCHANGE = "topic_order_exchange";
    public static final String TOPIC_QUEUE_NAME_1 = "test_topic_queue_1";
    public static final String TOPIC_ROUTINGKEY_1 = "test.*";

    @Bean
    public TopicExchange topicExchange() {
        return new TopicExchange(TOPIC_EXCHANGE);
    }

    @Bean
    public Queue topicQueue1() {
        // 建立引數 Map 容器
        Map<String, Object> args = new HashMap<>();
        // 設定訊息過期時間 注意此處是數值 5000 不是字串
        args.put("x-message-ttl", 5000);
        // 設定佇列過期時間
        args.put("x-expires", 8000);
        // 在最後傳入額外引數 即這些過期資訊
        return new Queue(TOPIC_QUEUE_NAME_1, true, false, false, args);
    }

    @Bean
    public Binding bindingTopic1() {
        return BindingBuilder.bind(topicQueue1())
                .to(topicExchange())
                .with(TOPIC_ROUTINGKEY_1);
    }
}
  1. 建立引數 Map 容器:型別是在 Queue 引數中所要求的,要按照要求來。
  2. 設定訊息過期時間:這裡設定的訊息過期時間,會應用到所有訊息中。
  3. 設定佇列過期時間
  4. 傳入額外引數:將上述配置好的過期時間設定,通過 Queue 傳入即可。
  • 生產者
@SpringBootTest(classes = RabbitmqSpringbootApplication.class)
@RunWith(SpringRunner.class)
public class RabbitMqTest {
    /**
     * 注入 RabbitTemplate
     */
    @Autowired

    @Test
    public void testTopicSendMessage() {
        rabbitTemplate.convertAndSend(RabbitMqConfiguration.TOPIC_EXCHANGE, "test.order.insert", "This is a message !");
    }
}

不要配置消費者,然後就可以在 Web 管理器中看到效果了

6.1.2 應用於單獨訊息的過期時間

  • 配置中保持最初的樣子就行了,就不需要配置過期時間了
  • 生產者中配置訊息單獨的過期時間
@SpringBootTest(classes = RabbitmqSpringbootApplication.class)
@RunWith(SpringRunner.class)
public class RabbitMqTest {
    /**
     * 注入 RabbitTemplate
     */
    @Autowired

    @Test
    public void testTopicSendMessage2() {
        MessagePostProcessor messagePostProcessor = new MessagePostProcessor(){
            public Message postProcessMessage(Message message){
                // 注意此處是 字串 “5000”
                message.getMessageProperties().setExpiration("5000");
                message.getMessageProperties().setContentEncoding("UTF-8");
                return message;
            }
        };
        rabbitTemplate.convertAndSend(RabbitMqConfiguration.TOPIC_EXCHANGE, "test.order",
                "This is a message 002 !",messagePostProcessor);
    }
}

6.2 死信佇列

死信官方原文為 Dead letter ,它是RabbitMQ中的一種訊息機制,當你在消費訊息時,如果佇列以及佇列裡的訊息出現以下情況,說明當前訊息就成為了 “死信”,如果配置了死信佇列,這些資料就會傳送到其中,如果沒有配置就會直接丟棄。

  1. 訊息被拒絕
  2. 訊息過期
  3. 佇列達到最大長度

不過死信佇列並不是什麼很特殊的存在,我們只需要配置一個交換機,在消費的那個佇列中配置,出現死信就重新傳送到剛才配置的交換機中去,進而被路由到與交換機繫結的佇列中去,這個佇列也就是死信佇列,所以從建立上看,它和普通的佇列沒什麼區別。

6.2.1 應用場景

比如在一些比較重要的業務佇列中,未被正確消費的訊息,往往我們並不想丟棄,因為丟棄後如果想恢復這些資料,往往需要運維人員從日誌獲取到原訊息,然後重新投遞訊息,而配置了死信佇列,相當於給了未正確消費訊息一個暫存的位置,日後需要恢復的時候,只需要編寫對應的程式碼就可以了。

6.2.2 實現方式

  • 定義一個處理死信的交換機和佇列
@Configuration
public class DeadRabbitMqConfiguration{

    @Bean
    public DirectExchange deadDirect(){
        return new DirectExchange("dead_direct_exchange");}

    @Bean
    public Queue deadQueue(){
        return new Queue("dead_direct_queue");}
    @Bean
    public Binding deadBinds(){
        return BindingBuilder.bind(deadQueue()).to(deadDirect()).with("dead");
    }
}
  • 在正常的消費佇列中指定死信佇列
@Configuration
public class RabbitMqConfiguration {

    public static final String TOPIC_EXCHANGE = "topic_order_exchange";
    public static final String TOPIC_QUEUE_NAME_1 = "test_topic_queue_1";
    public static final String TOPIC_ROUTINGKEY_1 = "test.*";

    @Bean
    public TopicExchange topicExchange() {
        return new TopicExchange(TOPIC_EXCHANGE);
    }

    @Bean
    public Queue topicQueue1() {
        // 設定過期時間
        Map<String, Object> args = new HashMap<>();
        args.put("x-message-ttl", 5000);
        // 設定死信佇列交換器
        args.put("x-dead-letter-exchange","dead_direct_exchange");
        // 設定交換路由的路由key fanout 模式不需要配置此條
        args.put("x-dead-letter-routing-key","dead");
        return new Queue(TOPIC_QUEUE_NAME_1, true, false, false, args);
    }

    @Bean
    public Binding bindingTopic1() {
        return BindingBuilder.bind(topicQueue1())
                .to(topicExchange())
                .with(TOPIC_ROUTINGKEY_1);
    }
}

6.3 記憶體及磁碟監控

6.3.1 記憶體告警及控制

為了防止避免伺服器因記憶體不夠而崩潰,所以 RabbitMQ 設定了一個閾值,當記憶體使用量超過閾值的時候,RabbitMQ 會暫時阻塞所有客戶端的連線,並且停止繼續接受新訊息。

有兩種方式可以修改這個閾值

  1. 通過命令(二選一即可)
    • 命令的方式會在 Broker 重啟後失效
# 通過百分比設定的命令 <fraction> 處代表百分比小數例如 0.6
rabbitmqctl set_vm_memory_high_watermark <fraction>
# 通過絕對值設定的命令 <value> 處代表設定的一個固定值例如 700MB
rabbitmqctl set_vm_memory_high_watermark absolute <value>
  1. 通過修改配置檔案 rabbitmq.conf
    • 配置檔案每次啟動都會載入,屬於永久有效
# 百分比設定 預設值為 0.4 推薦 0.4-0.7 之間
vm_memory_high_watermark.relative = 0.5
# 固定值設定
vm_memory_high_watermark.absolute = 2GB

6.3.2 記憶體換頁

在客戶端連線和生產者被阻塞之前,它會嘗試將佇列中的訊息換頁到磁碟中,這種思想在作業系統中其實非常常見,以最大程度的滿足訊息的正常處理。

當記憶體換頁發生後,無論持久化還是非持久化的訊息,都會被轉移到磁碟,而由於持久化的訊息本來就在磁碟中有一個持久化的副本,所以會優先移除持久化的訊息。

預設情況下,當記憶體達到閾值的 50 % 的時候,就會進行換頁處理。

可以通過設定 vm_memory_high_watermark_paging_ratio 修改

# 值小於 1, 如果大於 1 就沒有意義了
vm_memory_high_watermark_paging_ratio = 0.6

6.3.3 磁碟預警

如果無止境的換頁,也很有可能會導致耗盡磁碟空間導致伺服器崩潰,所以 RabbitMQ 又提供了一個磁碟預警的閾值,當低於這個值的時候就會進行報警,預設是 50MB,可以通過命令的方式修改

# 固定值
rabbitmqctl set_disk_free_limit <disk_limit>
# 百分數
rabbitmqctl set_disk_free_limit memory_limit <fraction>

6.4 訊息的可靠傳遞

生產者向 RabbitMQ 中傳送訊息的時候,可能會因為網路等種種原因導致傳送失敗,所以 RabbitMQ 提供了一系列保證訊息可靠傳遞的機制,可以大致分為生產者和消費者兩部分的處理

6.4.1 生產者中的機制

生產者作為訊息的傳送者,需要保證自己的訊息傳送成功,RabbitMQ 提供了兩種方式來保證這一點。-

  1. confirm 確認模式
  2. return 退回模式

6.4.1.1 confirm 確認模式

生產者傳送訊息後,會非同步等待接收一個 ack 應答,收到返回的 ack 確認訊息後,根據 ack是 true 還是 false,呼叫 confirmCallback 介面進行處理

  • 配置類
spring:
  rabbitmq:
    # 傳送確認
    publisher-confirm-type: correlated
  • 實現 ConfirmCallback 介面的 confirm 方法
@Component
public class ConfirmCallbackService implements RabbitTemplate.ConfirmCallback {

    /**
     * @param correlationData 相關配置資訊
     * @param ack             exchange交換機 是否成功收到了訊息。true 成功,false代表失敗
     * @param cause           失敗原因
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            //接收成功
            System.out.println("訊息成功傳送到交換機");
        } else {
            //接收失敗
            System.out.println("訊息傳送到交換機失敗,失敗原因: " + cause);
            // TODO 可以處理失敗的訊息,例如再次傳送等等
        }
    }
}
  • 宣告佇列和交換機
@Configuration
public class RabbitMqConfig {
    @Bean()
    public Queue confirmTestQueue() {
        return new Queue("confirm_test_queue", true, false, false);
    }

    @Bean()
    public FanoutExchange confirmTestExchange() {
        return new FanoutExchange("confirm_test_exchange");
    }

    @Bean
    public Binding confirmTestFanoutExchangeAndQueue() {
        return BindingBuilder.bind(confirmTestQueue()).to(confirmTestExchange());
    }
}
  • 生產者
@SpringBootTest(classes = RabbitmqSpringbootApplication.class)
@RunWith(SpringRunner.class)
public class RabbitMqTest {
    /**
     * 注入 RabbitTemplate
     */
    @Autowired
    
     /**
     * 注入 ConfirmCallbackService
     */
    @Autowired
    private ConfirmCallbackService confirmCallbackService;

    @Test
    public void testConfirm() {
        // 設定確認回撥類
        rabbitTemplate.setConfirmCallback(confirmCallbackService);
        // 傳送訊息
        rabbitTemplate.convertAndSend("confirm_test_exchange", "", "ConfirmCallback !");
    }
}

6.4.1.2 return 退回模式

當 Exchange 傳送到 Queue 失敗時,會呼叫一個 returnsCallback,我們可以通過實現這個介面,然後來處理這種失敗的情況。

  • 在配置檔案中開啟傳送回撥
spring:
  rabbitmq:
    # 傳送回撥
    publisher-returns: true
  • 實現 ReturnsCallback 的 returnedMessage 方法
//  public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) 已經屬於過時方法了
@Component
public class ReturnCallbackService implements RabbitTemplate.ReturnsCallback {
    @Override
    public void returnedMessage(ReturnedMessage returned) {
        System.out.println(returned);
    }
}
  • 宣告佇列和交換機(Direct 模式)
@Configuration
public class RabbitMqConfig {

    @Bean()
    public Queue returnsTestQueue() {
        return new Queue("return_test_queue", true, false, false);
    }

    @Bean()
    public DirectExchange returnsTestExchange() {
        return new DirectExchange("returns_test_exchange");
    }

    @Bean
    public Binding returnsTestDirectExchangeAndQueue() {
        return BindingBuilder.bind(returnsTestQueue()).to(returnsTestExchange()).with("info");
    }
}
  • 生產者
@SpringBootTest(classes = RabbitmqSpringbootApplication.class)
@RunWith(SpringRunner.class)
public class RabbitMqTest {
    /**
     * 注入 RabbitTemplate
     */
    @Autowired
    
    /**
     * 注入 ConfirmCallbackService
     */
    @Autowired
    private ConfirmCallbackService confirmCallbackService;
    
    /**
     * 注入 ReturnCallbackService
     */
    @Autowired
    private ReturnCallbackService returnCallbackService;

    @Test
    public void testReturn() {
        // 確保訊息傳送失敗後可以重新返回到佇列中
        rabbitTemplate.setMandatory(true);
        // 訊息投遞到佇列失敗回撥處理
        rabbitTemplate.setReturnsCallback(returnCallbackService);
        // 訊息投遞確認模式
        rabbitTemplate.setConfirmCallback(confirmCallbackService);
        // 傳送訊息
        rabbitTemplate.convertAndSend("returns_test_exchange", "info", "ReturnsCallback !");
    }
}
  • 修改不同的路由key,即可測試出結果。

6.4.2 消費者中的機制

6.4.2.1 ack 確認機制

ack 表示收到訊息的確認,預設是自動確認,但是它有三種型別

acknowledge-mode 選項介紹

  • auto:自動確認,為預設選項
  • manual:手動確認(按能力分配就需要設定為手動確認)
  • none:不確認,傳送後自動丟棄

其中自動確認是指,當訊息一旦被消費者接收到,則自動確認收到,並把這個訊息從佇列中刪除。

但是在實際業務處理中,正確的接收到的訊息可能會因為業務上的問題,導致訊息沒有正確的被處理,但是如果設定了 手動確認方式,則需要在業務處理成功後,呼叫channel.basicAck(),手動簽收,如果出現異常,則呼叫 channel.basicNack()方法,讓其自動重新傳送訊息。

  • 配置檔案
spring:
  rabbitmq:
    listener:
      simple:
      	# 手動確認
        acknowledge-mode: manual 
  • 消費者
@Component
@RabbitListener(queues = "confirm_test_queue")
public class TestConsumer {

    @RabbitHandler
    public void processHandler(String msg, Channel channel, Message message) throws IOException {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            System.out.println("訊息內容: " + new String(message.getBody()));

            System.out.println("業務出錯的位置:");
            int i = 66 / 0;
            
            // 手動簽收 deliveryTag標識代表佇列可以刪除了
            channel.basicAck(deliveryTag, true);
        } catch (Exception e) {
            // 拒絕簽收
            channel.basicNack(deliveryTag, true, true);
        }
    }
}

6.5 叢集 & 6.6 分散式事務(待更新)

由於這兩個點篇幅也不短,實在不願草草簡單寫上了事,放到後面單獨的文章編寫,釋出哇。

關於叢集的搭建暫時可參考:https://blog.csdn.net/belonghuang157405/article/details/83540148