天天看點

上司:誰再用定時任務實作關閉訂單,立馬滾蛋!

在電商、支付等領域,往往會有這樣的場景,使用者下單後放棄支付了,那這筆訂單會在指定的時間段後進行關閉操作,細心的你一定發現了像某寶、某東都有這樣的邏輯,而且時間很準确,誤差在1s内;那他們是怎麼實作的呢?

文章轉載:樂位元組

一般的做法有如下幾種

定時任務關閉訂單

rocketmq延遲隊列

rabbitmq死信隊列

時間輪算法

redis過期監聽

一、定時任務關閉訂單(最low)

一般情況下,最不推薦的方式就是關單方式就是定時任務方式,原因我們可以看下面的圖來說明

上司:誰再用定時任務實作關閉訂單,立馬滾蛋!

我們假設,關單時間為下單後10分鐘,定時任務間隔也是10分鐘;通過上圖我們看出,如果在第1分鐘下單,在第20分鐘的時候才能被掃描到執行關單操作,這樣誤差達到10分鐘,這在很多場景下是不可接受的,另外需要頻繁掃描主訂單号造成網絡io和磁盤io的消耗,對實時交易造成一定的沖擊,是以pass

二、rocketmq延遲隊列方式

延遲消息 生産者把消息發送到消息伺服器後,并不希望被立即消費,而是等待指定時間後才可以被消費者消費,這類消息通常被稱為延遲消息。 在rocketmq開源版本中,支援延遲消息,但是不支援任意時間精度的延遲消息,隻支援特定級别的延遲消息。 消息延遲級别分别為1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,共18個級别。

發送延遲消息(生産者)

消費延遲消息(消費者)

實作監聽類,處理具體邏輯

這種方式相比定時任務好了很多,但是有一個緻命的缺點,就是延遲等級隻有18種(商業版本支援自定義時間),如果我們想把關閉訂單時間設定在15分鐘該如何處理呢?顯然不夠靈活。

三、rabbitmq死信隊列的方式

rabbitmq本身是沒有延遲隊列的,隻能通過rabbitmq本身隊列的特性來實作,想要rabbitmq實作延遲隊列,需要使用rabbitmq的死信交換機(exchange)和消息的存活時間ttl(time to live)

死信交換機 一個消息在滿足如下條件下,會進死信交換機,記住這裡是交換機而不是隊列,一個交換機可以對應很多隊列。

一個消息被consumer拒收了,并且reject方法的參數裡requeue是false。也就是說不會被再次放在隊列裡,被其他消費者使用。 上面的消息的ttl到了,消息過期了。

隊列的長度限制滿了。排在前面的消息會被丢棄或者扔到死信路由上。 死信交換機就是普通的交換機,隻是因為我們把過期的消息扔進去,是以叫死信交換機,并不是說死信交換機是某種特定的交換機

消息ttl(消息存活時間) 消息的ttl就是消息的存活時間。rabbitmq可以對隊列和消息分别設定ttl。對隊列設定就是隊列沒有消費者連着的保留時間,也可以對每一個單獨的消息做單獨的設定。超過了這個時間,我們認為這個消息就死了,稱之為死信。如果隊列設定了,消息也設定了,那麼會取值較小的。是以一個消息如果被路由到不同的隊列中,這個消息死亡的時間有可能不一樣(不同的隊列設定)。這裡單講單個消息的ttl,因為它才是實作延遲任務的關鍵。

可以通過設定消息的expiration字段或者x-message-ttl屬性來設定時間,兩者是一樣的效果。隻是expiration字段是字元串參數,是以要寫個int類型的字元串:當上面的消息扔到隊列中後,過了60秒,如果沒有被消費,它就死了。不會被消費者消費到。這個消息後面的,沒有“死掉”的消息對頂上來,被消費者消費。死信在隊列中并不會被删除和釋放,它會被統計到隊列的消息數中去

處理流程圖

上司:誰再用定時任務實作關閉訂單,立馬滾蛋!

建立交換機(exchanges)和隊列(queues)

建立死信交換機

上司:誰再用定時任務實作關閉訂單,立馬滾蛋!

如圖所示,就是建立一個普通的交換機,這裡為了友善區分,把交換機的名字取為:delay

建立自動過期消息隊列 這個隊列的主要作用是讓消息定時過期的,比如我們需要2小時候關閉訂單,我們就需要把消息放進這個隊列裡面,把消息過期時間設定為2小時

上司:誰再用定時任務實作關閉訂單,立馬滾蛋!

建立一個一個名為delay_queue1的自動過期的隊列,當然圖檔上面的參數并不會讓消息自動過期,因為我們并沒有設定x-message-ttl參數,如果整個隊列的消息有消息都是相同的,可以設定,這裡為了靈活,是以并沒有設定,另外兩個參數x-dead-letter-exchange代表消息過期後,消息要進入的交換機,這裡配置的是delay,也就是死信交換機,x-dead-letter-routing-key是配置消息過期後,進入死信交換機的routing-key,跟發送消息的routing-key一個道理,根據這個key将消息放入不同的隊列

建立消息處理隊列 這個隊列才是真正處理消息的隊列,所有進入這個隊列的消息都會被處理

上司:誰再用定時任務實作關閉訂單,立馬滾蛋!

消息隊列的名字為delay_queue2 消息隊列綁定到交換機 進入交換機詳情頁面,将建立的2個隊列(delayqueue1和delayqueue2)綁定到交換機上面

上司:誰再用定時任務實作關閉訂單,立馬滾蛋!

自動過期消息隊列的routing key 設定為delay 綁定delayqueue2

上司:誰再用定時任務實作關閉訂單,立馬滾蛋!

delayqueue2 的key要設定為建立自動過期的隊列的x-dead-letter-routing-key參數,這樣當消息過期的時候就可以自動把消息放入delay_queue2這個隊列中了 綁定後的管理頁面如下圖:

上司:誰再用定時任務實作關閉訂單,立馬滾蛋!

當然這個綁定也可以使用代碼來實作,隻是為了直覺表現,是以本文使用的管理平台來操作 發送消息

設定了讓消息6秒後過期 注意:因為要讓消息自動過期,是以一定不能設定delay_queue1的監聽,不能讓這個隊列裡面的消息被接受到,否則消息一旦被消費,就不存在過期了

接收消息 接收消息配置好delay_queue2的監聽就好了

這種方式可以自定義進入死信隊列的時間;是不是很完美,但是有的小夥伴的情況是消息中間件就是rocketmq,公司也不可能會用商業版,怎麼辦?那就進入下一節

四、時間輪算法

上司:誰再用定時任務實作關閉訂單,立馬滾蛋!

(1)建立環形隊列,例如可以建立一個包含3600個slot的環形隊列(本質是個數組)

(2)任務集合,環上每一個slot是一個set 同時,啟動一個timer,這個timer每隔1s,在上述環形隊列中移動一格,有一個current index指針來辨別正在檢測的slot。

task結構中有兩個很重要的屬性: (1)cycle-num:當current index第幾圈掃描到這個slot時,執行任務 (2)訂單号,要關閉的訂單号(也可以是其他資訊,比如:是一個基于某個訂單号的任務)

假設目前current index指向第0格,例如在3610秒之後,有一個訂單需要關閉,隻需: (1)計算這個訂單應該放在哪一個slot,當我們計算的時候現在指向1,3610秒之後,應該是第10格,是以這個task應該放在第10個slot的set中 (2)計算這個task的cycle-num,由于環形隊列是3600格(每秒移動一格,正好1小時),這個任務是3610秒後執行,是以應該繞3610/3600=1圈之後再執行,于是cycle-num=1

current index不停的移動,每秒移動到一個新slot,這個slot中對應的set,每個task看cycle-num是不是0: (1)如果不是0,說明還需要多移動幾圈,将cycle-num減1 (2)如果是0,說明馬上要執行這個關單task了,取出訂單号執行關單(可以用單獨的線程來執行task),并把這個訂單資訊從set中删除即可。 (1)無需再輪詢全部訂單,效率高 (2)一個訂單,任務隻執行一次 (3)時效性好,精确到秒(控制timer移動頻率可以控制精度)

五、redis過期監聽

1.修改redis.windows.conf配置檔案中notify-keyspace-events的值 預設配置notify-keyspace-events的值為 "" 修改為 notify-keyspace-events ex 這樣便開啟了過期事件

2. 建立配置類redislistenerconfig(配置redismessagelistenercontainer這個bean)

3.繼承keyexpirationeventmessagelistener建立redis過期事件的監聽類

4:測試 通過redis用戶端存一個有效時間為3s的訂單:

上司:誰再用定時任務實作關閉訂單,立馬滾蛋!

結果:

上司:誰再用定時任務實作關閉訂單,立馬滾蛋!

總結: 以上方法隻是個人對于關單的一些想法,可能有些地方有疏漏,請在公衆号直接留言進行指出,當然如果你有更好的關單方式也可以随時溝通交流