RabbitMQ消息處理失敗,我們會讓失敗消息進入重試隊列等待執行,因爲在重試隊列距離真正執行還需要定義的時間間隔,因此,我們可以將重試隊列設置成延時處理。今天參考網上其他人的實現,簡單梳理下消息延時重試執行的思路。
消費失敗後,自動延時將消息重新投遞,當達到一定的重試次數後,將消息投遞到失敗消息隊列,等待人工介入處理。在這裏我們一步一步實現一個帶有失敗重試功能的發佈訂閱組件,使用該組件後可以非常簡單的實現消息的發佈訂閱。
業務背景
- 結合RabbitMQ的Topic模式和Work Queue模式實現生產方產生消息,消費方按需訂閱,消息投遞到消費方的隊列之後,多個worker同時對消息進行消費
- 結合RabbitMQ的 Message TTL 和 Dead Letter Exchange 實現消息的延時重試功能
- 消息達到最大重試次數之後,將其投遞到失敗隊列,等待人工介入處理bug後,重新將其加入隊列消費
執行流程圖
- 生產者發佈消息到主Exchange
- 主Exchange根據Routing Key將消息分發到對應的消息隊列
- 多個消費者的worker進程同時對隊列中的消息進行消費,因此它們之間採用“競爭”的方式來爭取消息的消費
- 消息消費後,不管成功失敗,都要返回ACK消費確認消息給隊列,避免消息消費確認機制導致重複投遞,同時,如果消息處理成功,則結束流程,否則進入重試階段
- 如果重試次數小於設定的最大重試次數(默認爲3次),則將消息重新投遞到Retry Exchange的重試隊列
- 重試隊列不需要消費者直接訂閱,它會等待消息的有效時間過期之後,重新將消息投遞給Dead Letter Exchange,我們在這裏將其設置爲主Exchange,實現延時後重新投遞消息,這樣消費者就可以重新消費消息
- 如果三次以上都是消費失敗,則認爲消息無法被處理,直接將消息投遞給Failed Exchange的Failed Queue,這時候應用可以觸發報警機制,以通知相關責任人處理
- 等待人工介入處理(解決bug)之後,重新將消息投遞到主Exchange,這樣就可以重新消費了
技術實現:
創建Exchange
爲了實現消息的延時重試和失敗存儲,我們需要創建三個Exchange來處理消息。
- master 主Exchange,發佈消息時發佈到該Exchange
- master.retry 重試Exchange,消息處理失敗時(3次以內),將消息重新投遞給該Exchange
- master.failed 失敗Exchange,超過三次重試失敗後,消息投遞到該Exchange
所有的Exchange聲明(declare)必須使用以下參數
參數 | 值 | 說明 |
---|---|---|
exchange | - | Exchange名稱 |
type | topic | Exchange 類型 |
passive | false | 如果Exchange已經存在,則返回成功,不存在則創建 |
durable | true | 持久化存儲Exchange,這裏僅僅是Exchange本身持久化,消息和隊列需要單獨指定其持久化 |
no-wait | false | 該方法需要應答確認 |
在RabbitMQ的管理界面中,我們可以看到創建的三個Exchange
消息發佈
消息發佈時,使用basic_publish
方法,參數如下
參數 | 值 | 說明 |
---|---|---|
message | - | 發佈的消息對象 |
exchange | master | 消息發佈到的Exchange |
routing-key | - | 路由KEY,用於標識消息類型 |
mandatory | false | 是否強制路由,指定了該選項後,如果沒有訂閱該消息,則會返回路由不可達錯誤 |
immediate | false | 指定了當消息無法直接路由給消費者時如何處理 |
發佈消息時,對於message
對象,其內容使用json編碼後的字符串,同時消息進行持久化
消息訂閱
消息訂閱的實現相對複雜一些,需要完成隊列的聲明以及隊列和Exchange的綁定
Declare Queue
對於每一個訂閱消息的服務,都必須創建一個該服務對應的隊列,將該隊列綁定到關注的路由規則,這樣之後,消息生產者將消息投遞給Exchange之後,就會按照路由規則將消息分發到對應的隊列供消費者消費了。
消費服務需要declare三個隊列
[queue_name]
隊列名稱,格式符合[服務名稱]@訂閱服務標識
[queue_name]@retry
重試隊列[queue_name]@failed
失敗隊列
Declare隊列時,參數規定規則如下
參數 | 值 | 說明 |
---|---|---|
queue | - | 隊列名稱 |
passive | false | 隊列不存在則創建,存在則直接成功 |
durable | true | 隊列持久化 |
exclusive | false | 排他,指定該選項爲true則隊列只對當前連接有效,連接斷開後自動刪除 |
no-wait | false | 該方法需要應答確認 |
auto-delete | false | 當不再使用時,是否自動刪除 |
對於@retry
重試隊列,需要指定額外參數
'x-dead-letter-exchange' => 'master'
'x-dead-letter-routing-key' => [queue_name],
'x-message-ttl' => 30 * 1000 // 重試時間設置爲30s
這裏的兩個header字段的含義是,在隊列中延遲30s後,將該消息重新投遞到x-dead-letter-exchange
對應的Exchange中,並且routing key指定爲消費隊列的名稱,這樣就可以實現消息只投遞給原始出錯時的隊列,避免消息重新投遞給所有關注當前routing key的消費者了。
在RabbitMQ的管理界面中,Queues部分可以看到我們創建的三個隊列
查看隊列的詳細信息,我們可以看到 queueName@retry 隊列與其它兩個隊列的不同
隊列和Exchange綁定
創建完隊列之後,需要將隊列與Exchange綁定(bind
),不同隊列需要綁定到之前創建的對應的Exchange上面
Queue | Exchange |
---|---|
[queue_name] | master |
[queue_name]@retry | master.retry |
[queue_name]@failed | master.failed |
綁定時,需要提供訂閱的路由KEY,該路由KEY與消息發佈時的路由KEY對應,區別是這裏可以使用通配符同時訂閱多種類型的消息。
參數 | 值 | 說明 |
---|---|---|
queue | - | 綁定的隊列 |
exchange | - | 綁定的Exchange |
routing-key | - | 訂閱的消息路由規則 |
no-wait | false | 該方法需要應答確認 |
在RabbitMQ的管理界面中,我們可以看到該隊列與Exchange和routing-key的綁定關係
消息消費實現
使用 basic_consume
對消息進行消費的時候,需要注意下面參數
參數 | 值 | 說明 |
---|---|---|
queue | - | 消費的隊列名稱 |
consumer-tag | - | 消費者標識,留空即可 |
no_local | false | 如果設置了該字段,服務器將不會發布消息到 發佈它的客戶端 |
no_ack | false | 需要消費確認應答 |
exclusive | false | 排他訪問,設置後只允許當前消費者訪問該隊列 |
nowait | false | 該方法需要應答確認 |
消費端在消費消息時,需要從消息中獲取消息被消費的次數,以此判斷該消息處理失敗時重試還是發送到失敗隊列。
在消息發送到重試隊列和失敗隊列時,我們在消息的headers中添加了一個名爲x-orig-routing-key
的字段,該字段是實現消息重試的關鍵字段,由於我們的消息需要在不同的Exchange,Queue之間流轉,爲了避免消息在重新投遞到主Exchange時,被所有的消費者隊列重新消費,在重試過程中,我們將消息的routing-key修改爲隊列名稱,直接投遞給原始消費消息的隊列。x-orig-routing-key
用於在之後能夠重新獲取到最開始的routing-key。
這裏的重複消費是指 某個消息被兩個消費方A和B消費了,其中A消費失敗,B成功,這時候,消息由A消費者重新投遞到主Exchange後,B消費隊列也會獲取到該消息,因此就會導致B消費者重複消費已經消費國的消息
本文實現延時重試,使用了三個重試Exchange,Exchange如果訂閱特別多的話,Exchange的壓力會非常大,因此在非常極端的情況下,消息大批量失敗,且消息收發非常快,那麼Exchange的性能可能會有問題。
本文是使用發佈訂閱實現延時重試的消息執行,也會有其他思路。