黃可 王盛義 胡兵 李朝陽 易勇
摘?要:延遲隊列是一種延遲處理消息的特殊隊列,在實際應(yīng)用系統(tǒng)中,存在大量需要延遲處理業(yè)務(wù)的場景。常規(guī)的輪詢檢測實現(xiàn)方式,存在諸多弊端。本文結(jié)合具體業(yè)務(wù)場景,利用RabbitMQ消息中間件的死信機(jī)制,設(shè)計實現(xiàn)了一種簡單優(yōu)雅的延遲隊列。
關(guān)鍵詞:延遲隊列;RabbitMQ;死信
1.RabbitMQ和延遲隊列
1.1RabbitMQ中間件
RabbitMQ是當(dāng)下非常流行的開源消息隊中間件,使用erlang語言開發(fā),實現(xiàn)了AMQP協(xié)議。由于其性能穩(wěn)定,維護(hù)更新快,社區(qū)活躍,廣泛應(yīng)用于眾多企業(yè)信息系統(tǒng)中。
RabbitMQ的工作原理[1]如圖1所示。其中P表示消息的生產(chǎn)者(Producer),X表示交換機(jī)(Exchange),Q表示隊列(Queue),C表示消息的消費者(Consumer)。
AMQP協(xié)議規(guī)定的消息系統(tǒng)應(yīng)包括消息生產(chǎn)者、消息消費者和消息服務(wù)器三個核心功能組件。其中RabbitMQ服務(wù)器上可以劃分為多個虛擬主機(jī)(vhost),每個虛擬機(jī)都可以創(chuàng)建交換機(jī)和隊列,虛擬機(jī)之間的交換機(jī)和隊列數(shù)據(jù)是互相隔離的。
交換機(jī)接收消息生產(chǎn)者發(fā)布的消息,并根據(jù)規(guī)則將消息路由給符合條件的隊列。交換機(jī)是匹配和路由消息的實體。
隊列是存儲和轉(zhuǎn)發(fā)消息的實體,隊列中的消息會被順序傳遞給一個或多個消費者。
綁定(binding)用于表示交換機(jī)和消息隊列(或交換機(jī))之間的關(guān)系。消息生產(chǎn)者發(fā)送消息時提供了消息的路由參數(shù)(routing key),交換機(jī)收到消息后匹配綁定規(guī)則,然后將消息投遞到指定的隊列。
1.2延遲隊列
延遲隊列是一種特殊的隊列,和一般的隊列不同,延遲隊列中的消息不會被立即消費,往往需要延遲一定的時間后才會被消費。延遲隊列中的消息有別于定時任務(wù),定時任務(wù)通常是有明確的觸發(fā)時間和觸發(fā)周期的。而延遲隊列中的消息沒有固定的開始時間,消息進(jìn)入延遲隊列后間隔指定的時間執(zhí)行。
在應(yīng)用系統(tǒng)中,延遲隊列的應(yīng)用場景很常見的。例如:春運12306搶票,顧客45分鐘內(nèi)沒有完成支付,系統(tǒng)自動取消訂單。用戶一周內(nèi)沒有活躍記錄時,系統(tǒng)向不活躍的用戶發(fā)送郵件等。
延遲隊列有多種實現(xiàn)方式,可以使用Java自帶的DealayQueue,使用定時任務(wù)輪詢,使用Redis[2]等。當(dāng)然,也可以通過RabbitMQ的死信機(jī)制實現(xiàn)。
2.延遲隊列的設(shè)計
2.1RabbitMQ的死信機(jī)制
死信(Dead Letter)是RabbitMQ中的一種消息機(jī)制,在消費消息時,如果隊列中的消息出現(xiàn)以下三種情形:
1、消息或者隊列的TTL過期;
2、消息被消費端拒絕(basic.reject or basic.nack)并且消息不重新進(jìn)入隊列(requeue?=false);
3、隊列中的消息數(shù)量已達(dá)到最大值。
那么該消息即為死信。RabbitMQ會對死信進(jìn)行特殊處理,如果配置了死信交換機(jī)和死信隊列信息,死信將會路由到死信隊列中,如果沒有配置,該死信消息將被丟棄。
2.2基于死信機(jī)制實現(xiàn)延遲隊列
RabbitMQ消息中間件[3]中沒有延遲隊列類型,但可以通過死信機(jī)制,使普通隊列具備延遲隊列的特性。死信機(jī)制的核心是配置隊列中消息的生存時間(TTL,Time To Live),死信交換機(jī)(DLX,Dead Letter Exchange)和死信路由(DLK,Dead Letter Key)?;谒佬艡C(jī)制實現(xiàn)的延遲隊列基本結(jié)構(gòu)如圖2所示。
可以看到,配置一個延遲隊列,基本分為以下三個步驟:
1、聲明業(yè)務(wù)隊列Q,并綁定到業(yè)務(wù)交換機(jī)X上;
2、配置業(yè)務(wù)隊列Q的x-message-ttl,x-dead-letter-exchange和x-dead-letter-routing-key屬性;
3、聲明死信隊列DLQ,并綁定到死信交換機(jī)DLX上。
和圖1不同,除了聲明正常的業(yè)務(wù)隊列Q之外,還聲明了一個死信隊列DLQ。和RabbitMQ中的普通隊列相同,死信隊列DLQ只是和死信交換機(jī)DLX建立了綁定。業(yè)務(wù)隊列Q中的消息超過了設(shè)置的生存時間x-message-ttl后,會根據(jù)隊列上設(shè)置的x-dead-letter-exchange和x-dead-letter-routing-key屬性,將超時的消息發(fā)送到設(shè)定的死信交換機(jī)中。其中x-message-ttl用于設(shè)置隊列中消息的生存時間,x-dead-letter-exchange用于指定死信交換機(jī)的名稱,x-dead-letter-routing-key用于設(shè)定死信消息的路由鍵。死信交換機(jī)根據(jù)路由鍵將死信消息路由到符合匹配規(guī)則的死信隊列DLQ中。最后由消費者C消費已經(jīng)延遲了指定的時間的消息,從而實現(xiàn)了延遲隊列的功能。
3.延遲隊列的應(yīng)用
3.1應(yīng)用場景介紹
電商平臺促銷搶購時,訂單超時關(guān)閉是延遲隊列的一種典型應(yīng)用場景。其不僅有延遲處理業(yè)務(wù)的基本要求,還有低資源消耗,高可用的潛在要求。傳統(tǒng)方式一般是通過開啟定時器的方式輪詢掃描符合條件的業(yè)務(wù)數(shù)據(jù),然后再進(jìn)行延遲處理。由于定時任務(wù)有固定的觸發(fā)周期,可能存在很多訂單已經(jīng)超時,但還沒到處理訂單任務(wù)的觸發(fā)時刻,無法及時處理的情況。另一方面,促銷搶購是一個大數(shù)據(jù)量、高并發(fā)的業(yè)務(wù)場景,使用定時器頻繁掃描數(shù)據(jù)庫中大量未付款的訂單,將給數(shù)據(jù)庫和應(yīng)用服務(wù)器帶來巨大的壓力。而使用RabbitMQ的延遲隊列,則可以優(yōu)雅地應(yīng)對該場景。
3.2應(yīng)用場景分析實踐
實際的業(yè)務(wù)場景中,訂單超時關(guān)閉涉及到多種業(yè)務(wù)處理邏輯,例如商品出庫,退回庫存的處理。簡單起見,暫不討論過程中的庫存處理邏輯。以訂單45分鐘內(nèi)未支付,訂單自動關(guān)閉為例,用戶下單時,需要將用戶的下單記錄存儲至數(shù)據(jù)庫中,同時也需要將用戶訂單id作為消息發(fā)送到隊列中,并設(shè)置隊列中消息的生存時間為45分鐘,再配置隊列的x-dead-letter-exchange和x-dead-letter-routing-key屬性信息,指明隊列中的訂單45分鐘后需要投遞到指定的死信交換機(jī),并路由到指定的死信隊列中處理。消費端監(jiān)聽到死信隊列中的到期的訂單信息后,根據(jù)訂單的id在用戶下單記錄表中查詢該訂單的支付狀態(tài)。如果用戶在45分鐘已完成支付或者取消了該訂單,則直接向用戶返回相應(yīng)的提示信息。如果用戶未支付也沒有取消訂單,表明用戶未在指定時間內(nèi)完成支付,則需要將訂單狀態(tài)更新為關(guān)閉狀態(tài)。
和定時輪詢相比,使用延遲隊列后,訂單的檢查時間精確,而且不會進(jìn)行全表查詢。在搶購場景下,45分鐘內(nèi)可能產(chǎn)生數(shù)萬條訂單,使用延遲隊列可以避免定時頻繁查詢數(shù)據(jù)庫,有效減輕內(nèi)存、CPU、網(wǎng)絡(luò)和數(shù)據(jù)庫服務(wù)器的負(fù)載。
4.總結(jié)
本文介紹了延遲隊列的基本概念和常見的實現(xiàn)方式。重點介紹了RabbitMQ的工作原理和RabbitMQ延遲隊列的構(gòu)造過程。并以訂單支付超時自動關(guān)閉這一具體場景說明如何使用RabbitMQ構(gòu)造的延遲隊列進(jìn)行訂單檢查處理。
目前該延遲隊列的功能比較簡單,也沒有做成公共服務(wù)提供給開發(fā)人員使用。需進(jìn)一步增強(qiáng)隊列的故障恢復(fù),消息履歷追蹤等功能,并進(jìn)行封裝簡化。
參考文獻(xiàn)
[1]?王冬雪. 基于AMQP的異構(gòu)信息轉(zhuǎn)換/傳輸機(jī)制的研究與實現(xiàn)[D]. 杭州:浙江工業(yè)大學(xué),2013.
[2]?劉子辰. 基于 Tair/Redis 的延遲隊列系統(tǒng)的設(shè)計與實現(xiàn)[D]. 大連:大連理工大學(xué),2017.
[3]?馬巍,武欣嶸,鄭翔,等.RabbitMQ在實時監(jiān)控系統(tǒng)中的應(yīng)用[J]. 軍事通信技術(shù),2017,38(1):83.