面試官:延遲隊列有哪些實現方案?說說你的見解

延遲隊列的需求各位應該在平常開發的場景中常常碰到。好比:面試

用戶登陸以後5分鐘給用戶作分類推送;redis

用戶多少天未登陸給用戶作召回推送;算法

按期檢查用戶當前退款帳單是否被商家處理等等場景。數組

通常這種場景和定時任務仍是有很大的區別,定時任務是你知道任務多久該跑一次或者何時只跑一次,這個時間是肯定的。延遲隊列是當某個事件發生的時候須要延遲多久觸發配套事件,引子事件發生的時間不是固定的。數據結構

業界目前也有不少實現方案,單機版的方案就不說了,如今也沒有哪一個公司仍是單機版的服務,今天咱們一一探討各類方案的大體實現。多線程

1. Redis zset

這個方案比較經常使用,簡單有效。利用 Redis 的 sorted set 結構,使用 timeStamp 做爲 score,好比你的任務是要延遲5分鐘,那麼就在當前時間上加5分鐘做爲 score ,輪詢任務每秒只輪詢 score 大於當前時間的 key便可,若是任務支持有偏差,那麼當沒有掃描到有效數據的時候能夠休眠對應時間再繼續輪詢。架構

方案優劣異步

優勢:ide

簡單實用,一針見血。工具

缺點:

  1. 單個 zset 確定支持不了太大的數據量,若是你有幾百萬的延遲任務需求,大哥我仍是勸你換一個方案;
  2. 定時器輪詢方案可能會有異常終止的狀況須要本身處理,同時消息處理失敗的回滾方案,您也要本身處理。

因此,sorted set 的方案並非一個成熟的方案,他只是一個快速可供落地的方案。

2. RabbitMQ隊列

下面說一個能夠落地的方案,這個方案也被大多數目前在架構中使用了 RabbitMQ 的項目組使用。很差的一點就是,捆綁 RabbitMQ,當你的架構方案是要用別的 MQ 替換 RabbitMQ 的時候,你就蛋疼了(我如今正在經歷)。

RabbitMQ 有兩個特性,一個是 Time-To-Live Extensions,另外一個是 Dead Letter Exchanges

  • Time-To-Live Extensions

    RabbitMQ容許咱們爲消息或者隊列設置TTL(time to live),也就是過時時間。TTL代表了一條消息可在隊列中存活的最大時間,單位爲毫秒。也就是說,當某條消息被設置了TTL或者當某條消息進入了設置了TTL的隊列時,這條消息會在通過TTL秒後 「死亡」,成爲Dead Letter。若是既配置了消息的TTL,又配置了隊列的TTL,那麼較小的那個值會被取用。

  • Dead Letter Exchanges

    在 RabbitMQ 中,一共有三種消息的 「死亡」 形式:

    1. 消息被拒絕。經過調用 basic.reject 或者 basic.nack 而且設置的 requeue 參數爲 false;
    2. 消息由於設置了TTL而過時;
    3. 隊列達到最大長度。

DLX同通常的 Exchange 沒有區別,它能在任何的隊列上被指定,實際上就是設置某個隊列的屬性。當隊列中有 DLX 消息時,RabbitMQ就會自動的將 DLX 消息從新發布到設置的 Exchange 中去,進而被路由到另外一個隊列,publish 能夠監聽這個隊列中消息作相應的處理。

由上簡介你們能夠看出,RabbitMQ自己是不支持延遲隊列的,只是他的特性讓勤勞的 中國脫髮羣體 急中生智(爲了完成任務)弄出了這麼一套可用的方案。

可用的方案就是

  1. 若是有事件須要延遲那麼將該事件發送到MQ 隊列中,爲須要延遲的消息設置一個TTL;
  2. TTL到期後就會自動進入設置好的DLX,而後由DLX轉發到配置好的實際消費隊列;
  3. 消費該隊列的延遲消息,處理事件。

方案優劣

優勢:

大品牌組件,用的放心。若是面臨大數據量需求能夠很容易的橫向擴展,同時消息支持持久化,有問題可回滾。

缺點:

  1. 配置麻煩,額外增長一個死信交換機和一個死信隊列的配置;
  2. RabbitMQ 是一個消息中間件,TTL 和 DLX 只是他的一個特性,將延遲隊列綁定在一個功能軟件的某一個特性上,可能會有風險。不要槓,當大家組不用 RabbitMQ 的時候遷移很痛苦;
  3. 消息隊列具備先進先出的特色,若是第一個進入隊列的消息 A 的延遲是10分鐘,第二個進入隊列的消息B 的延遲是5分鐘,指望的是誰先到 TTL誰先出,可是事實是B已經到期了,而還要等到 A 的延遲10分鐘結束A先出以後,B 才能出。因此在設計的時候須要考慮不一樣延遲的消息要放到不一樣的隊列。

3. 基於 Netty#HashedWheelTimer類方法的實現

HashedWheelTimer 是 Netty 中 的一個基礎工具類,主要用來高效處理大量定時任務,且任務對時間精度要求相對不高, 在Netty 中的應用場景就是鏈接超時或者任務處理超時,通常都是操做比較快速的任務,缺點是內存佔用相對較高。

算法思想

HashedWheelTimer 主要仍是一個 DelayQueue 和一個時間輪算法組合。

面試官:延遲隊列有哪些實現方案?說說你的見解

Hash Wheel Timer是一個環形結構,能夠想象成時鐘,分爲不少格子,一個格子表明一段時間(越短Timer精度越高),並用一個List保存在該格子上到期的全部任務。同時一個指針隨着時間流逝一格一格轉動,並執行對應List中全部到期的任務。

以上圖爲例,假設一個格子是1s,則整個時間輪能表示的時間段16s。當前任務指向格子2,代表在第2s的時候有任務須要執行。任務列表中有兩個任務,每一個任務前面的數字表示圈數。2表示當走到第2圈的時候纔會執行,那麼整個任務的真正執行時間實際上是在12s以後執行,即第二圈走到2的時候。每推動一格,對應的每個 slot 中的round數都要減一。總體算法就是這麼個邏輯。

時間輪設計要點:

  • tick,一次時間推動,每次推動會檢查/執行超時任務;
  • tickDuration,時間輪推動的最小單元,每隔 tickDuration 會有一次 tick,它決定了時間輪的精確程度;
  • bucket(ticksPerWheel),上圖中的每一隔就是一個bucket,表示一個時間輪能夠有多少個tick,它是存儲任務的最小單元;
  • 上層時間輪的 tickDuration 是下層時間輪的表示時間的最大範圍,即:父 tickDuration = 子 tickDuration * 子 bucket 。

須要注意的是,這種方式任務是串行執行的。意味着你若是在時間輪中執行任務且任務耗時較長,將會出現調度超時或者任務堆積的狀況。因此要將任務的執行異步化。

算法的要點:

  1. 任務並非直接放在格子中的,而是維護了一個雙向鏈表,這種數據結構很是便於插入和移除;
  2. 新添加的任務並不直接放入格子,而是先放入一個隊列中,這是爲了不多線程插入任務的衝突。在每一個tick運行任務以前由worker線程自動對任務進行歸集和分類,插入到對應的槽位裏面。

Netty 使用數組 + 雙向鏈表的方式來組織時間輪,對於添加/取消操做僅作了記錄,真正的操做實際發生在下一個tick。時間的推動是獨立的線程在作,該線程同時也負責過時任務的執行等操做,可簡單認爲此步驟操做爲O(n),由於推動線程須要徹底遍歷timeoutscancelledTimeoutsbucket鏈表,在遍歷timeouts時,Netty爲了不任務過多,因此限制每次最多遍歷10萬個,也就是說,一個tick只能規劃10萬個任務,當任務量過大時,會存在超時任務執行時間延遲的現象。

方案優劣

優勢:

實現比較優雅。效率高。

缺點:

  1. 沒法實現HA和橫向擴展,要麼就使用多個時間輪。
  2. 最重要的是,實現也比較複雜,開發者須要考慮全部可能的狀況。

目前我瞭解到的延遲隊列在生產環境下有如上三種實現方式,每一種都有人在使用。固然沒有最好的只有最適合的,你以爲 redis 能知足需求,就按照最簡單的來,你要是有充足的開發週期,你也能夠實現時間輪展示實力。

需求千萬種,變化就一種:給時間都能作。

相關文章
相關標籤/搜索