開心一刻
晚上,媳婦和兒子躺在沙發上
兒子疑惑的問道:媽媽,你為什麼不去上班
媳婦:媽媽的人生目標是前20年靠父母養,後40年靠你爸爸養,再往後20年就靠你和妹妹養
兒子:我可養不起
媳婦:為什麼
兒子:因為,呃...,我和你的想法一樣
講在前面
如果你們對 RabbitMQ
感到陌生,那可以停止往下閱讀了
請先去查閱相關資料,對它有一個基本的瞭解之後再接著閱讀本文
本文會以循序漸進的方式來講解標題:
使用 RabbitMQ 的延遲佇列來實現:訂單在30分鐘之內未支付則自動取消
所以請你們耐心逐步往下看
另外,實現標題的方式有很多,但本文只講其中之一的 延遲佇列
,至於其他方式,不在本文講解範圍之內,如果想了解,煩請你們自行去查閱
訊息何去何從
RabbitMQ
的模型架構,相信你們都知道
訊息
由 Producer
生成,經 Exchange
路由到 Queue
,然後推給 Consumer
進行消費
消費訊息有兩種方式
- 推模式(Basic.Consume)
- 拉模式(Basic.Get)
如果 訊息
經 Exchange
無法路由到符合條件的佇列時,該 訊息
該如何處理,是返還給 Producer
還是直接丟棄?
如果 訊息
被路由到 Queue
時發現沒有任何消費者,該 訊息
該如何處理,是存在 Queue
中還是返還給 Producer
?
作為一個牛皮的中介軟體,一旦涉及到可選項了,應該怎麼做?
我相信你們已經想到了,那肯定是增加配置引數來支援可選項嘛!
mandatory
mandatory
引數用於設定訊息是否必須被路由到佇列中,預設值是 false
當 mandatory
引數設定為 true
時,Exchange
無法根據自身的型別和路由鍵找到一個符合條件的 Queue
,那麼 RabbitMQ
會呼叫 Basic.Return
命令將訊息返回給生產者。當 mandatory
引數設定為 false
時,出現上述情形,則訊息直接被丟棄
當 mandatory
值為 false
時
程式碼執行正常,但沒有輸出結果,所以我們不確定訊息是否投遞了
但我們可以透過 RabbitMQ
管理介面,看 Exchange
概況
來確定訊息確實投遞了
當 mandatory
值為 true
時,需要新增一個監聽器 ReturnListener
程式碼執行正常,同時也有輸出結果
2024-06-01 14:54:52|AMQP Connection 10.5.108.226:5672|com.qsl.rabbit.PriorityMessageTest|INFO|59|Basic.Return 返回結果:mandatory test
也可以透過 RabbitMQ
管理介面,看 Exchange
概況來確定訊息是否投遞過
作為擴充,給你們留兩個問題
- mandatory 設定為 true 的同時,不新增監聽器 ReturnListener,會是什麼結果
- mandatory 設定為 false 的同時,新增監聽器 ReturnListener,又會是什麼結果
immediate
immediate
引數用於設定訊息是否立即傳送給消費者,預設值是 false
當 immediate
引數設定為 true
時,如果訊息路由到佇列時發現佇列上並沒有任何消費者,那麼該訊息不會存入佇列中,當與路由鍵匹配的所有佇列都沒有消費者時,該訊息會透過 Basic.Return
返回至生產者
immediate 為 true ,訊息路由到匹配的佇列時
- 部分佇列有消費者,有消費者的佇列會立即將訊息投遞給消費者,沒有消費者的佇列會丟棄該訊息
- 全部佇列都沒有消費者,則將該訊息返回給生產者
執行如下程式碼
你會發現報錯
2024-06-01 16:16:06|AMQP Connection 10.5.108.226:5672|org.springframework.amqp.rabbit.connection.CachingConnectionFactory|ERROR|1575|Channel shutdown: connection error; protocol method: #method<connection.close>(reply-code=540, reply-text=NOT_IMPLEMENTED - immediate=true, class-id=60, method-id=40)
這是因為從 RabbitMQ 3.0
版本開始去掉了對 immediate
引數的支援,對此官方解釋如下
immediate
引數會影響映象佇列的效能,增加了程式碼複雜性,建議採用TTL
和DLX
替代immediate
概括來講,mandatory
針對的是訊息能否路由到至少一個佇列中,否則將訊息返回給生產者。immediate
針對的是訊息能否立即投遞給消費者,否則將訊息直接返回給生產者,不用將訊息存入佇列而等待消費者
Alternate Exchange
生產者在傳送訊息時,如果不設定 mandatory
引數(或設定為 false
),那麼訊息在未被路由的情況下會丟失;如果設定了 mandatory
(且設定成 true
),那麼需要新增對應的 ReturnListener
邏輯,生產者的程式碼會變得複雜。如果既不想增加生產者的複雜,又不想訊息丟失,那麼就可以使用備份交換器(Alternate Exchange
),將未被路由的訊息儲存在 RabbitMQ
中,在需要的時候再去處理這些訊息
實現程式碼如下
執行如下測試程式碼
訊息透過 com.qsl.normal.exchange
,經路由鍵 123
未匹配到任何佇列,此時訊息就會傳送給 com.qsl.normal.exchange
的備份交換器 com.qsl.alternate.exchange
,因為備份交換器的型別是 fanout
,所以訊息會被路由到 com.qsl.alternate.exchange
繫結的所有佇列上,目前只有一個佇列 com.qsl.unrouted.queue
,所以訊息最終來到 com.qsl.unrouted.queue
,訊息流轉如下
在 RabbitMQ
控制檯看佇列狀況如下
備份交換器和普通的交換器沒有太大的區別,為了方便使用,推薦選擇 fanout
型別;你們也可以選擇其他型別,比如 direct
或 topic
,但此時需要保證訊息被重新路由到備份交換器的路由鍵和生產者發出的路由鍵是一樣的,否則訊息不能正確路由到備份交換器的佇列中,訊息會丟失!
關於備份交換器,以下幾種特殊情況需要注意
- 如果設定的備份交換器不存在,客戶端和 RabbitMQ 伺服器都不會產生異常,此時訊息丟失
- 如果備份交換器沒有繫結任何佇列,客戶端和 RabbitMQ 伺服器都不會產生異常,此時訊息丟失
- 如果備份交換器沒有任何匹配的佇列,客戶端和 RabbitMQ 伺服器都不會產生異常,此時訊息丟失
- 如果備份交換器和 mandatory 引數一起使用,mandatory 會失效
過期時長(TTL)
TTL,Time to Live 的簡稱,字面意思生存時長,也有很多人稱過期時間,個人更習慣稱過期時長
訊息的 TTL
RabbitMQ
有兩種方法對訊息設定過期時長
- 透過佇列屬性設定,佇列中的所有訊息都有相同的過期時長
- 對訊息本身進行單獨設定,每條訊息的過期時長可以不同
如果兩種方法一起使用,則訊息的過期時長以兩者之間較小值為準(而非單純的以訊息的過期時長為準)
訊息在佇列中的生存時間一旦超過設定的過期時長,就會變成 死信(Dead Message)
,消費者將無法再透過正常的路由收到該訊息
可以透過繫結
死信佇列
來消費Dead Message
透過佇列屬性 x-message-ttl
可以設定訊息的過期時長,單位是毫秒,示例程式碼如下
如果不設定 TTL
,訊息不會過期;如果 TTL
設定成 0,則表示除非此時可以將訊息直接投遞給消費者,否則該訊息直接被丟棄,這個特性是不是看起來很眼熟?回過頭去看看 immediate
為 true
時的第 1 個特性
1.部分佇列有消費者,有消費者的佇列會立即將訊息投遞給消費者,沒有消費者的佇列會丟棄該訊息
透過引數 expiration
可以單獨設定每個訊息的過期時長,單位也是毫秒,示例程式碼如下
這兩種方法的過期策略是怎樣的,大家思考下再往下看
對於設定佇列屬性 x-message-ttl
的方法,佇列中的訊息具有相同的過期時長,佇列中已過期的訊息肯定是在佇列頭部,RabbitMQ
只需要定期的從隊頭開始往隊尾掃描,一旦訊息過期則從佇列中剔除,一旦掃描到 未過期
的訊息,則本次掃描完成
對於設定引數 expiration
的方法,每個訊息可以設定不同的過期時長,那麼過期的訊息不一定在佇列頭部,如果要刪除佇列中所有過期訊息,只能掃描整個佇列,此時的成本是比較高的,所以採用惰性刪除,即訊息即將被投遞給消費者時做過期判定,如果過期則進行刪除
如果既設定了佇列屬性 x-message-ttl
,又設定了 expiration
,那該如何判定訊息是否過期了呢?
定期刪除 + 惰性刪除,
Redis
的過期策略是不是也是這個?
佇列的 TTL
這裡針對的是佇列,而非佇列中的訊息,大家別和 訊息的 TTL
搞混了
透過引數 x-expires
可以設定佇列被自動刪除前處於未使用狀態的時長,單位是毫秒,不能設定為 0
未使用狀態需要滿足三點
- 佇列上沒有任何消費者
- 佇列也沒有被重新宣告
- 過期時間段內未呼叫過
Basic.Get
命令
RabbitMQ
能保證在過期時長到達後將佇列刪除,但不保障及時。RabbitMQ
重啟後,持久化的佇列的過期時長會被重新計算
如下是建立一個過期時長為 30 分鐘的佇列
佇列資訊如下
死信佇列
講 死信佇列
之前,我們得先了解 DLX
,全稱 Dead-Letter-Exchange
,中文翻譯成 死信交換器
當訊息在一個佇列中變成死信之後
訊息變成死信的情況包括以下3種
- 訊息被決絕(
Basic.Reject/Basic.Nack
),並設定引數requeue
為false
- 訊息過期
- 佇列達到最大長度
它能被重新傳送到另一個交換器中,這個交換器就是 DLX
,而繫結到 DLX
的佇列就是 死信佇列
DLX
也是一個正常的交換器,和一般的交換器沒有區別,它可以和任何佇列進行繫結,當繫結的佇列中存在死信時,RabbitMQ
就會自動將這個訊息重新發布到設定的 DLX
上,進而被路由到 死信佇列
。死信佇列
也是可以被監聽的,也可以有消費者對 死信佇列
中的訊息進行消費處理的
所以,死信佇列
可以變相的實現 immediate
為 true
時的第 2 種情況
2.全部佇列都沒有消費者,則將該訊息返回給生產者
為什麼是 變相
,因為不是直接將訊息返回給生產者,而是生產者可以監聽 死信佇列
,使訊息回到生產者;雖然結果一致,但實現方式還是有區別的
那麼 immediate
為 true
的特性,就可以用 TTL + 死信佇列
來替代了
透過引數 x-dead-letter-exchange
可以給佇列新增 DLX
;透過引數 x-dead-letter-routing-key
可以給這個 DLX
指定路由鍵,如果未配置該引數,則使用原佇列的路由鍵,實現程式碼如下
執行如下測試程式碼
訊息透過交換器 com.qsl.normal.exchange
,經路由鍵 ttlMessage
匹配到佇列 com.qsl.message.ttl.queue
中,佇列設定了 x-message-ttl
為 3000 毫秒,這段時長內佇列上沒有消費者消費這條訊息,訊息過期。由於給佇列設定了死信交換器 com.qsl.dlx.exchange
,訊息會透過該交換器,經路由鍵 dlx_routing_key
匹配到佇列 com.qsl.dlx.queue
中,訊息最終儲存在該死信佇列中,訊息流轉如下
RabbitMQ
控制檯,可以看到佇列狀況如下
DLX
是一個非常有用的特性,它可以處理異常情況下,訊息不能夠被消費者正確消費而被置入到死信佇列中,保證訊息不被丟失;後續分析程式可以透過消費死信佇列中的訊息來分析當時所遇到的異常情況,進而改善和最佳化系統
DLX
還有一個很重要的功能,它配合 TTL
可以實現延遲佇列,具體實現請往下看
延遲佇列
延遲佇列儲存的物件是延遲訊息
延遲訊息
指的是需要延遲消費的訊息就是當訊息傳送之後,並不想讓消費者立即拿到訊息,而是等待特定時長後,消費者才拿到訊息進行消費
延遲佇列的使用場景有很多,例如:
- 訂單系統中,下單完成之後 30 分鐘內完成支付,否則取消訂單
- 使用者註冊成功後,如果三天內沒有登陸則進行簡訊提醒
- 遠端控制掃地機器人,2 個小時後進行房間打掃
- ...
RabbitMQ
本身並沒有直接支援 延遲佇列
的功能,但是可以透過 DLX
和 TTL
模擬出 延遲佇列
的功能,具體實現已經在上一節(死信佇列
)中完成了,你們可以網上翻一翻
給大家演示 場景1
的完整示例,時間改成 1 分鐘內完成支付
生產者端配置
消費者端配置
訊息傳送
輸出日誌如下
實際應用中,可以根據延遲時長給延遲佇列劃分多個等級,例如
目前 RabbitMQ
提供了另外的方式來實現 延遲佇列
https://github.com/rabbitmq/rabbitmq-delayed-message-exchange
感興趣的可以去看看
總結
-
示例程式碼:spring-boot-rabbitmq
-
mandatory 與 immediate
mandatory
針對的是訊息能否路由到至少一個佇列中,否則將訊息返回給生產者immediate
針對的是訊息能否立即投遞給消費者,否則將訊息直接返回給生產者,不用將訊息存入佇列而等待消費者RabbitMQ 3.0
版本開始去掉了對immediate
引數的支援,可以用DLX
和TTL
來代替 -
過期時長
訊息的過期時長有兩種設定方式:佇列的引數
x-message-ttl
和訊息的引數expiration
佇列也可以設定過期時長,該時長內佇列一直處於未使用狀態則會被刪除;透過佇列引數
x-expires
來設定 -
死信佇列
繫結到死信交換器(
DLX
)上的佇列就是死信佇列DLX
能夠保證異常的情況下訊息不會丟失,後續透過分析死信佇列中的訊息,可以改善和最佳化系統 -
延遲佇列
目前來講,實現延遲佇列的方式有兩種
DLX
與TTL
- rabbitmq-delayed-message-exchange
參考
《RabbitMQ實戰指南》