背景數據庫
在本身接觸到的業務系統中,不少地方會有定時任務的需求,好比支付的交易超時自動關閉、鏈接超時、支付異步通知等等。常見的作法有:數組
1.考慮使用JDK中的Timer定時任務來實現緩存
2.經過封裝quartz搭建專門的調度平臺來管理數據結構
目前項目中運用的是第2種。異步
看到netty中hashedwheeltimer原理,本身能夠仿造一種數據結構,用來實現延時消息觸發。spa
首先分析項目中哪些運用場景,經過延時的過程當中數據的是否須要檢測最終是否觸發來劃分靜態的延時和動態的延時。線程
1、靜態延時:不須要在延時的過程當中判斷是否觸發定時任務,只是單純地到指定時間觸發任務便可,例如:交易成功通知業務系統。設計
2、動態延時:在延時的過程當中並非每一個任務都須要執行,是有前提條件才能觸發執行;例如:心跳檢測,鏈接超時等。3d
場景一分析:支付成功異步通知指針
支付模塊有一筆訂單支付成功通知業務系統的定時任務,具體是支付流水交易若是支付成功了,那麼由調度平臺根據cron定義的時間來觸發通知的任務。
假設支付流水錶的結構爲:t_jnl(jnl_no, pay_status,notify_status …),定時任務每一分鐘執行一次:目前的場景能夠簡化爲:
1.查詢出支付成功的流水記錄:select jnl_no from t_jnl where pay_status = 1 and notify_status =0;
2.調用業務系統接口,通知支付結果;
存在的問題:
①若是支付記錄數很大,那麼去查找知足條件的記錄會形成數據庫很大的壓力。僅僅根據2個狀態來查詢的效率是很低的。
每次查詢表數據,已經被執行過記錄,仍然會被掃描(只是不會出如今結果集中),有重複計算的嫌疑。
②若是知足條件的支付流水足夠多的話,至少每次不能一次性讀取。須要分頁查詢,這將會是一個for循環。目前作法是定時任務觸發時一次讀取100條數據。
若是記錄數超時定時任務中設定的數量(100),那麼在後面的記錄不會再本次中獲得執行。
③假如一條記錄剛好在剛執行任務後0.1s知足條件了(pay_status = 1),那麼幾乎要等待下一個週期被執行,時效性很差。偏差時間有可能就是cron的設置時間t。
場景一改造:(靜態延時)
爲了解決上述場景存在的問題,引入下面的設計:右側是經過一個數組進行封裝的環形隊列,相似一個時鐘。根據cron來設置環形隊列的segment,理解爲一個獨立的任務單元。左側是每一個任務單元的結構實現:set<Task>
以當前場景爲例,cron設置時間t=60s,n=60,後臺啓動一個timer,這個timer每隔1s,在上述環形隊列中移動一格,有一個Current Index指針來標識正在檢測的segment。
那麼改造的場景變爲:
1.在支付成功後根據current Index所在位置和cron設置週期確認在環形隊列上的segment下標和cyclenum後將數據插入環形隊列中。
假設 current Index = 3,想要在60s後執行,數據插入第3+60=63個節點,可是環形隊列最大長度爲60,因此cyclenum=63/60=1,segment=3
2.task function是具體執行延時任務的方法
假設異步通知業務系統的方法爲syncOrder(jnl_no) ,通知業務系統這筆流水支付成功了。
3.後臺一致啓動一個Timer,每隔t/n時間段,current index移動一個segment,當移動到當前的segment時候,渠道set<Task>中的cyclenum,
判斷是否爲0,若是cyclenum=0,當即執行task function(jnlno)(能夠用單獨的線程來執行Task),並把這個Task從Set<Task>中刪除,不然cyclenum -1。等待下個週期。
結論分析:
(1)無需與數據庫進行交互,不用再輪詢所有訂單,效率高
(2)時效性好,精確到秒(設置timer的移動頻率t和segment數量n能夠控制精度)
(3)可是須要考慮數據量大的時候內存吃緊的狀況(能夠經過t/n的頻率來減小內存中緩存的數據)。
場景二分析:支付成功但通知失敗後進行重複通知策略
在上面的"支付成功通知"場景中會去異步通知業務系統,根據業務系統響應後修改通知狀態.有時候會出現業務系統宕機或者超時的狀況,遇到此種問題須要再次發起通知。
1.系統目前的解決辦法是:查詢出支付成功但通知失敗的流水記錄:select jnl_no from t_jnl where pay_status = 1 and notify_status =2;
2.再次調用業務系統接口,通知支付結果;
3.修改對應的通知狀態,若是通知成功後續不會再通知,失敗還會發起通知。
存在的問題:
①若是「支付成功但通知失敗」記錄不多,那麼去查找的時候已經通知成功的記錄仍然會被掃描,只爲查詢少許數據但須要全盤掃描其實資源就被浪費了。
②假如一條記錄剛好在剛執行任務後0.1s知足條件了,那麼幾乎要等待下一個週期t=5min被執行,時效性很差。偏差時間有可能就是週期t。
場景二改造:(動態延時)
之因此是動態延遲是由於並非每次通知的結果都須要延遲執行任務,只有通知失敗纔會有後續的延時任務。
以當前場景爲例,首先在場景一中調用定時任務中的異步通知方法,若是通知失敗後將syncOrder(jnl_no)的流水號jnl_no存入Map數據中,將對應的環形隊列的下標存入Map的值。
cron設置時間t=300s,n=60,後臺啓動一個timer,這個timer每隔5s,在上述環形隊列中移動一格,有一個Current Index指針來標識正在檢測的segment。
那麼改造的場景變爲:
1.假設current index指向segment=3的時候,執行通知但結果失敗,先確認該流水下次在隊列上的index = current index -1 = 2 ,到下一次被執行恰好300s.
因此map.put(jnl_no,2),同時把curent index指向的節點從數據刪除。
2.隔了300s後上一步的segment會被current index讀取,執行通知任務,若是執行成功,把map中的數據刪除掉,執行失敗繼續按照上一步步驟進行。
哪些元素是通知失敗的呢?
Current Index每秒種移動一個segment,這個segment對應的Set<jnl_no>中全部jnl_no都應該被執行!若是最近500s有通知失敗的,必定被放到Current Index的前一個segment了,Current Index所在的segment對應Set中全部元素,都是通知失敗的。因此,當沒有通知失敗時,Current Index掃到的每個segment的Set中應該都沒有元素。
結論分析:
相對項目中目前的優點:
(1)只須要1個timer便可,無需數據庫交互,全局搜索。
(2)批量通知,Current Index掃到的segment,Set中全部元素都應該被從新發起通知。
除開上面目前項目中運用的方法,還有其餘的一些辦法,來進行比較下。
「輪詢掃描法」
1)用一個Map<jnl_no, last_notify_time>來記錄每個jnl_no最近一次通知時間last_notify_time
2)當某個用戶jnl_no通知失敗時,實時更新這個last_notify_time
3)啓動一個timer,當Map中不爲空時,輪詢掃描這個Map,看每一個jnl_no的last_notify_time是否超過500s,若是超過500s進行超時再次通知。
「多timer觸發法」
1)用一個Map<jnl_no, last_notify_time>來記錄每個jnl_no最近一次請求時間last_notify_time
2)當某個jnl_no有通知失敗,實時更新這個Map,並同時對這個jnl_no啓動一個timer,500s以後觸發
3)每一個jnl_no對應的timer觸發後,看Map中,查看這個jnl_no的last_notify_time是否超過500s,若是超過則進行通知處理
方案一:只啓動一個timer,但須要輪詢,效率較低
方案二:不須要輪詢,但每一個請求包要啓動一個timer,比較耗資源