不少時候,業務有「在一段時間以後,完成一個工做任務」的需求。數組
例如:滴滴打車訂單完成後,若是用戶一直不評價,48小時後會將自動評價爲5星。數據結構
通常來講怎麼實現這類「48小時後自動評價爲5星」需求呢?線程
常見方案:啓動一個cron定時任務,每小時跑一次,將完成時間超過48小時的訂單取出,置爲5星,並把評價狀態置爲已評價。設計
假設訂單表的結構爲:t_order(oid, finish_time, stars, status, …),更具體的,定時任務每隔一個小時會這麼作一次:3d
select oid from t_order where finish_time > 48hours and status=0;指針
update t_order set stars=5 and status=1 where oid in[…];blog
若是數據量很大,須要分頁查詢,分頁update,這將會是一個for循環。隊列
方案的不足:ci
(1)輪詢效率比較低消息隊列
(2)每次掃庫,已經被執行過記錄,仍然會被掃描(只是不會出如今結果集中),有重複計算的嫌疑
(3)時效性不夠好,若是每小時輪詢一次,最差的狀況下,時間偏差會達到1小時
(4)若是經過增長cron輪詢頻率來減小(3)中的時間偏差,(1)中輪詢低效和(2)中重複計算的問題會進一步凸顯
如何利用「延時消息」,對於每一個任務只觸發一次,保證效率的同時保證明時性,是今天要討論的問題。
2、高效延時消息設計與實現
高效延時消息,包含兩個重要的數據結構:
(1)環形隊列,例如能夠建立一個包含3600個slot的環形隊列(本質是個數組)
(2)任務集合,環上每個slot是一個Set<Task>
同時,啓動一個timer,這個timer每隔1s,在上述環形隊列中移動一格,有一個Current Index指針來標識正在檢測的slot。
Task結構中有兩個很重要的屬性:
(1)Cycle-Num:當Current Index第幾圈掃描到這個Slot時,執行任務
(2)Task-Function:須要執行的任務指針
假設當前Current Index指向第一格,當有延時消息到達以後,例如但願3610秒以後,觸發一個延時消息任務,只需:
(1)計算這個Task應該放在哪個slot,如今指向1,3610秒以後,應該是第11格,因此這個Task應該放在第11個slot的Set<Task>中
(2)計算這個Task的Cycle-Num,因爲環形隊列是3600格(每秒移動一格,正好1小時),這個任務是3610秒後執行,因此應該繞3610/3600=1圈以後再執行,因而Cycle-Num=1
Current Index不停的移動,每秒移動到一個新slot,這個slot中對應的Set<Task>,每一個Task看Cycle-Num是否是0:
(1)若是不是0,說明還須要多移動幾圈,將Cycle-Num減1
(2)若是是0,說明立刻要執行這個Task了,取出Task-Funciton執行(能夠用單獨的線程來執行Task),並把這個Task從Set<Task>中刪除
使用了「延時消息」方案以後,「訂單48小時後關閉評價」的需求,只需將在訂單關閉時,觸發一個48小時以後的延時消息便可:
(1)無需再輪詢所有訂單,效率高
(2)一個訂單,任務只執行一次
(3)時效性好,精確到秒(控制timer移動頻率能夠控制精度)
3、總結
環形隊列是一個實現「延時消息」的好方法,開源的MQ好像都不支持延遲消息,不妨本身實現一個簡易的「延時消息隊列」,能解決不少業務問題,並減小不少低效掃庫的cron任務。
另外,關於MQ的可達性、冪等性將來撰文另述。