一胧后、需求描述
在電商、支付等領(lǐng)域抱环,往往會有這樣的場景壳快,用戶下單后放棄支付了,那這筆訂單會在指定的時間段后進行關(guān)閉操作江醇,而且時間很準(zhǔn)確濒憋,誤差在1s內(nèi)。
二陶夜、實現(xiàn)方案
- 定時任務(wù)關(guān)閉訂單(不推薦)
- RocketMQ延遲隊列(不夠靈活)
- RabbitMQ死信隊列(不推薦)
- RabbitMQ的delay插件rabbitmq_delayed_message_exchange實現(xiàn)延時消息(推薦)
- 時間輪算法
- Redis過期監(jiān)聽
三、方案詳解
3.1 定時任務(wù)關(guān)閉訂單
一般情況下裆站,最不推薦的方式就是關(guān)單方式就是定時任務(wù)方式条辟。我們假設(shè),關(guān)單時間為下單后10分鐘宏胯,定時任務(wù)間隔也是10分鐘羽嫡,如果在第1分鐘下單,在第20分鐘的時候才能被掃描到執(zhí)行關(guān)單操作肩袍,這樣誤差達到10分鐘杭棵,這在很多場景下是不可接受的,另外需要頻繁掃描主訂單號造成網(wǎng)絡(luò)IO和磁盤IO的消耗,對實時交易造成一定的沖擊魂爪,所以PASS先舷。
3.2 RocketMQ延遲隊列方式
延遲消息 生產(chǎn)者把消息發(fā)送到消息服務(wù)器后,并不希望被立即消費滓侍,而是等待指定時間后才可以被消費者消費蒋川,這類消息通常被稱為延遲消息。 在RocketMQ開源版本中撩笆,支持延遲消息捺球,但是不支持任意時間精度的延遲消息,只支持特定級別的延遲消息夕冲。 消息延遲級別分別為1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h氮兵,共18個級別。
這種方式相比定時任務(wù)好了很多歹鱼,但是有一個致命的缺點泣栈,就是延遲等級只有18種(商業(yè)版本支持自定義時間),如果我們想把關(guān)閉訂單時間設(shè)置在15分鐘該如何處理呢醉冤?顯然不夠靈活秩霍。
3.3 RabbitMQ死信隊列
Rabbitmq本身是沒有延遲隊列的,只能通過Rabbitmq本身隊列的特性來實現(xiàn)蚁阳,想要Rabbitmq實現(xiàn)延遲隊列铃绒,需要使用Rabbitmq的死信交換機(Exchange)和消息的存活時間TTL(Time To Live)。
一個消息在滿足如下條件下螺捐,會進死信交換機颠悬,記住這里是交換機而不是隊列,一個交換機可以對應(yīng)很多隊列定血。
一個消息被Consumer拒收了赔癌,并且reject方法的參數(shù)里requeue是false。也就是說不會被再次放在隊列里澜沟,被其他消費者使用灾票。 上面的消息的TTL到了,消息過期了茫虽。
隊列的長度限制滿了刊苍,排在前面的消息會被丟棄或者扔到死信路由上。 死信交換機就是普通的交換機濒析,只是因為我們把過期的消息扔進去正什,所以叫死信交換機,并不是說死信交換機是某種特定的交換機号杏。
消息TTL(消息存活時間) 消息的TTL就是消息的存活時間婴氮。RabbitMQ可以對隊列和消息分別設(shè)置TTL。對隊列設(shè)置就是隊列沒有消費者連著的保留時間,也可以對每一個單獨的消息做單獨的設(shè)置主经。超過了這個時間荣暮,我們認(rèn)為這個消息就死了,稱之為死信旨怠。如果隊列設(shè)置了渠驼,消息也設(shè)置了,那么會取值較小的迷扇。所以一個消息如果被路由到不同的隊列中蜓席,這個消息死亡的時間有可能不一樣(不同的隊列設(shè)置)课锌。這里單講單個消息的TTL,因為它才是實現(xiàn)延遲任務(wù)的關(guān)鍵雏胃。
可以通過設(shè)置消息的expiration字段或者x-message-ttl屬性來設(shè)置時間志鞍,兩者是一樣的效果。只是expiration字段是字符串參數(shù)统翩,所以要寫個int類型的字符串此洲,當(dāng)上面的消息扔到隊列中后,過了60秒娶桦,如果沒有被消費趟紊,它就死了碰酝。不會被消費者消費到戴差。這個消息后面的,沒有“死掉”的消息對頂上來墨吓,被消費者消費纹磺。死信在隊列中并不會被刪除和釋放,它會被統(tǒng)計到隊列的消息數(shù)中去秘症。
3.4 RabbitMQ的delay插件rabbitmq_delayed_message_exchange實現(xiàn)延時消息
3.4.1 安裝插件
去RabbitMQ的官網(wǎng)下載插件乡摹,插件地址:https://www.rabbitmq.com/community-plugins.html
采转,直接搜索rabbitmq_delayed_message_exchange
即可找到我們需要下載的插件,下載和RabbitMQ配套的版本板熊。
3.4.2 與RabbitMQ死信隊列實現(xiàn)方式對比
- 死信隊列:死信隊列是這樣一個隊列察绷,如果消息發(fā)送到該隊列并超過了設(shè)置的時間克婶,就會被轉(zhuǎn)發(fā)到設(shè)置好的處理超時消息的隊列當(dāng)中去,利用該特性可以實現(xiàn)延遲消息鸭蛙。
- 延遲插件:通過安裝插件筋岛,自定義交換機睁宰,讓交換機擁有延遲發(fā)送消息的能力,從而實現(xiàn)延遲消息孝赫。
- 總結(jié):由于死信隊列方式需要創(chuàng)建兩個交換機(死信隊列交換機+處理隊列交換機)红符、兩個隊列(死信隊列+處理隊列)伐债,而延遲插件方式只需創(chuàng)建一個交換機和一個隊列峰锁,所以后者使用起來更簡單双戳。
3.5 時間輪算法
1魄衅、創(chuàng)建環(huán)形隊列膏斤,例如可以創(chuàng)建一個包含3600個slot的環(huán)形隊列(本質(zhì)是個數(shù)組)
2、任務(wù)集合莫辨,環(huán)上每一個slot是一個Set,同時啟動一個timer盘榨,這個timer每隔1s蟆融,在上述環(huán)形隊列中移動一格型酥,有一個Current Index指針來標(biāo)識正在檢測的slot。
Task結(jié)構(gòu)中有兩個很重要的屬性:
1郁竟、Cycle-Num:當(dāng)Current Index第幾圈掃描到這個Slot時由境,執(zhí)行任務(wù)虏杰。
2、訂單號:要關(guān)閉的訂單號(也可以是其他信息瘸彤,比如:是一個基于某個訂單號的任務(wù))
假設(shè)當(dāng)前Current Index指向第0格笛钝,例如在3610秒之后,有一個訂單需要關(guān)閉拯杠,只需:
1潭陪、計算這個訂單應(yīng)該放在哪一個slot依溯,當(dāng)我們計算的時候現(xiàn)在指向1瘟则,3610秒之后醋拧,應(yīng)該是第10格丹壕,所以這個Task應(yīng)該放在第10個slot的Set中。
2缭乘、計算這個Task的Cycle-Num琉用,由于環(huán)形隊列是3600格(每秒移動一格,正好1小時)奴紧,這個任務(wù)是3610秒后執(zhí)行绰寞,所以應(yīng)該繞3610/3600=1圈之后再執(zhí)行铣口,于是Cycle-Num=1
Current Index不停的移動,每秒移動到一個新slot脑题,這個slot中對應(yīng)的Set叔遂,每個Task看Cycle-Num是不是0。如果不是0蚕苇,說明還需要多移動幾圈,將Cycle-Num減1涩笤。如果是0蹬碧,說明馬上要執(zhí)行這個關(guān)單Task了,取出訂單號執(zhí)行關(guān)單(可以用單獨的線程來執(zhí)行Task)炒刁,并把這個訂單信息從Set中刪除即可恩沽。
1、無需再輪詢?nèi)坑唵蜗枋迹矢?br>
2罗心、一個訂單,任務(wù)只執(zhí)行一次
3绽昏、時效性好协屡,精確到秒(控制timer移動頻率可以控制精度)。
3.6 Redis過期監(jiān)聽
1全谤、修改redis.windows.conf
配置文件中notify-keyspace-events
的值肤晓,默認(rèn)配置notify-keyspace-events
的值為 "" 修改為 notify-keyspace-events Ex
這樣便開啟了過期事件
2、 創(chuàng)建配置類RedisListenerConfig
(配置RedisMessageListenerContainer
這個bean)认然。
3补憾、繼承KeyExpirationEventMessageListener
創(chuàng)建Redis過期事件的監(jiān)聽類盈匾。