本文來自網易雲社區。redis
時間輪實現
時間輪是一種環形的數據結構,分紅多個格。
每一個格表明一段時間,時間越短,精度越高。
每一個格上用一個鏈表保存在該格的過時任務。
指針隨着時間一格一格轉動,並執行相應格子中的到期任務。
名詞解釋:算法
- 時間格:環形結構中用於存放延遲任務的區塊
- 指針:指向當前操做的時間格,表明當前時間
- 格數:時間輪中時間格的個數
- 間隔:每一個時間格之間的間隔,表明時間輪能達到的精度
- 總間隔:當前時間輪總間隔,等於格數*間隔,表明時間輪能表達的時間範圍
單表時間輪數據庫
以上圖爲例,假設一個格子是1秒,則整個時間輪能表示的時間段爲8s, 若是當前指針指向2,此時須要調度一個3s後執行的任務,須要放到第5個格子(2+3)中,指針再轉3次就能夠執行了。
單表時間輪存在的問題是:
格子的數量有限,所能表明的時間有限,當要存放一個10s後到期的任務怎麼辦?這會引發時間輪溢出。
有個辦法是把輪次信息也保存到時間格鏈表的任務上。數據結構
若是任務要在10s後執行,算出輪次10/8 round等1,格子10%8等於2,因此放入第二格。
檢查過時任務時應當只執行round爲0的任務,鏈表中其餘任務的round減1。
帶輪次單表時間輪存在的問題是:
若是任務的時間跨度很大,數量很大,單層時間輪會形成任務的round很大,單個格子的鏈表很長,每次檢查的量很大,會作不少無效的檢查。怎麼辦?
分層時間輪架構
過時任務必定是在底層輪中被執行的,其餘時間輪中的任務在接近過時時會不斷的降級進入低一層的時間輪中。
分層時間輪中每一個輪都有本身的格數和間隔設置,當最低層的時間輪轉一輪時,高一層的時間輪就轉一個格子。
分層時間輪大大增長了可表示的時間範圍,同時減小了空間佔用。
舉個例子:
上圖的分層時間輪可表達8 8 8=512s的時間範圍,若是用單表時間輪可能須要512個格子, 而分層時間輪只要8+8+8=24個格子,若是要設計一個時間範圍是1天的分層時間輪,三個輪的格子分別用2四、60、60便可。
工做原理:
時間輪指針轉動有兩種方式:異步
- 根據本身的間隔轉動(秒鐘輪1秒轉1格;分鐘輪1分鐘轉1格;時鐘輪1小時轉1格)
- 經過下層時間輪推進(秒鐘輪轉1圈,分鐘輪轉1格;分鐘輪轉1圈,時鐘輪轉1格)
指針轉到特定格子時有兩種處理方式:數據庫設計
- 若是是底層輪,指針指向格子中鏈表上的元素均表示過時
- 若是是其餘輪,將格子上的任務移動到精度細一級的時間輪上,好比時鐘輪的任務移動到分鐘輪上
舉個例子: 分佈式
- 算出任務應該放在秒鐘輪的第5個格子
- 在秒鐘輪指針進行5次轉動後任務會被執行
- 算出該任務的延遲時間已經溢出秒鐘輪
- 50/8=6,因此該任務會被保存在分鐘輪的第6個格子
- 在秒鐘輪走了6圈(6*8s=48s)以後,分鐘輪的指針指向第6個格子
- 此時該格子中的任務會被降級到秒鐘輪,並根據50%8=2,任務會被移動到秒鐘輪的第2個格子
- 在秒鐘輪指針又進行2次轉動後(50s)任務會被執行
- 算出該任務的延遲時間已經溢出分鐘輪
- 250/8/8=3,因此該任務會被保存在時鐘輪的第3個格子
- 在分鐘輪走了3圈(3*64s=192s)以後,時鐘輪的指針指向第3個格子
- 此時該格子中的任務會被降級到分鐘輪,並根據(250-192)/8=7,任務會被移動到分鐘輪的第7個格子
- 在秒鐘輪走了7圈(7*8s=56s)以後,分鐘輪的指針指向第7個格子
- 此時該格子中的任務會被降級到秒鐘輪,並根據(250-192-56)=2,任務會被移動到秒鐘輪的第2個格子
- 在秒鐘輪指針又進行2次轉動後任務會被執行
優勢:性能
- 高性能(插入任務、刪除任務的時間複雜度均爲O(1),DelayQueue因爲涉及到排序,插入和移除的複雜度是O(logn))
缺點:線程
- 數據是保存在內存,須要本身實現持久化
- 不具有分佈式能力,須要本身實現高可用
- 延遲任務過時時間受時間輪總間隔限制
對於超出範圍的任務可放在一個緩衝區中(可用隊列、redis或數據庫實現),等最高時間輪轉到下一格子就從緩衝中取出符合範圍的任務落到時間輪中。
好比:
- 算出該任務的延遲時間已經溢出時間輪
- 因此任務被保存到緩衝隊列中
- 在時鐘輪走了1格以後,會從緩衝隊列中取知足範圍的任務落到時間輪中
- 緩衝隊列中的全部任務延遲時間均需減去64s,任務A減去64s後是536s,依然大於時間輪範圍,因此不會被移出隊列
- 在時鐘輪又走了1格以後,任務A減去64s是536-64=472s,在時間輪範圍內,會被落入時鐘輪
以前的設計(DB/DelayQueue/ZooKeeper)
調度系統提供任務操做接口供業務系統提交任務、取消任務、反饋執行結果等。
針對dubbo調用,將任務抽象成JobCallbackService接口,由業務系統實現並註冊成服務。
總體架構
數據庫:
內存隊列:
- 實際爲DelayQueue,延遲任務精確觸發的機制由它保證
- 只存儲將來N分鐘內過時且最多1000個任務
ZooKeeper:
- 管理整個調度集羣
- 存儲調度節點信息
- 存儲節點分片信息
主節點:
調度節點:
- 提供dubbo、http接口供業務系統調用,用於提交任務、取消任務、反饋執行結果等
- 從ZK註冊中心獲取當前節點的分片信息,再從數據庫拉取即將過時的數據放到DelayQueue
- 調用業務系統註冊的回調服務接口,發起調度請求
- 接收業務系統的反饋結果,更新執行結果,移除任務或發起重試
業務系統:
- 做爲被調度的服務須要實現回調接口JobCallbackService,並註冊爲dubbo服務提供者
- 在須要延遲任務的場景調用調度系統接口操做任務
數據庫設計
表說明
- job_callback_service:服務配置表,配置業務回調服務,包括服務協議、回調服務、重試次數
- job_delay_task:延遲任務表,用於存儲延遲任務,包括任務分片號、回調服務、調用總次數、失敗數、任務狀態、回調參數等
- job_delay_task_execlog:延遲任務執行表,記錄調度系統發起的每一次回調
- job_delay_task_backlog:延遲任務調度結果表,記錄任務最終狀態等信息
主從切換
利用ZooKeeper臨時序列節點特性,序號最小的節點爲主節點,其餘節點爲從節點。
主節點監聽集羣狀態,集羣狀態發生變化時從新分片。
從節點監聽序號比它小的兄弟節點,兄弟節點發生變化從新尋找和創建監聽關係。
數據分片
任務狀態
- delay:延遲任務提交後的初始狀態
- ready:過時時間已到,消息推入就緒隊列的狀態
- running:業務訂閱消息,收到消息開始處理的狀態
- finished:業務處理成功
- failed:業務處理失敗
主要流程
服務加載
- 從DB讀取服務配置
- 根據配置動態構造Consumer對象並添加到Spring容器中
提交任務
- 業務系統經過dubbo或http接口提交任務
- 判斷任務過時時間是否在一個掃描週期內
- 若是是,
- 設置分片號(從當前節點所負責的分片隨機獲取)
- 添加到內存隊列
- 任務保存到job_delay_task表
- 若是否,
- 設置分片號(根據分片總數和隨機算法算出分片號)
- 任務保存到delay_task表
定時器
- 由一個線程管理
- 根據配置的掃描間隔設置定時器的執行週期
- 根據當前時間和掃描間隔算出該時段的過時時間X-Delay
- 從DB獲取過時時間在X-Delay以前的全部任務,並放到DelayQueue
調度任務
- 由一個線程池管理
- 全部線程都阻塞在DelayQueue的方法take
- take到任務,從DB中獲取任務,判斷是否存在
- 若是不在,什麼也不作(任務已執行成功或已被刪除)
- 若是存在,判斷調用次數是否超過設置
- 若是不超
- 調用業務回調服務
- 從任務中取出調用的服務配置
- 從容器中獲取對應的Consumer對象
- 異步調用業務回調服務
- 設置下次重試時間,記錄調用日誌job_delay_task_execlog
- 若是超過,將任務轉移到job_delay_task_backlog
任務反饋
- 更新任務調用結果
優勢
缺點
- 略微複雜
- 須要將服務配置動態生成爲Consumer對象
- 增長新的服務須要通知全部調度節點刷新
- 存在必定的耦合性(直接調用業務服務,協議耦合),若是接入系統是thrift協議呢?
- 須要處理任務的重試
- 調度系統直接回調業務服務,若是業務服務不可用可能會形成盲目重試,不能很好的控制流量(調度系統不知道業務服務的處理能力)
若是引入MQ,使用MQ來解耦服務調用的協議,保證任務的重試,並由消費方根據本身的處理能力控制流量會不會更好呢?
另外一種方案(DB/DelayQueue/ZooKeeper/MQ)
總體架構
數據庫設計
主要流程
調度任務
- 由一個線程池管理
- 全部線程都阻塞在DelayQueue的take方法
- take到任務,從DB中獲取任務,判斷是否存在
- 若是不在,什麼也不作(任務已執行成功或已被刪除)
- 若是存在,將任務轉移到job_delay_task_execlog;往消息隊列投遞消息
缺點
須要業務系統依賴於MQ
本文來自網易雲社區,經做者陳志良受權發佈。
原文:延遲任務調度系統(技術選型與設計)