問題描述:做一個電商平臺,如何設置一個在買家下訂單后的”第60秒“發短信通知賣家發貨,需要考慮的是像淘寶一樣的大并發量的訂單。
原問題鏈接
最基礎設計
最直觀能想到的解決辦法就是使用延遲隊列的實現原理,其就是一個按時間排好序的隊列,每次put的時候排序,然后take的時候就計算時間是否過期,如果過期則返回隊列第一個元素進行消費,否則當前線程阻塞X秒后彈出第一個元素,這個就是DelayQueue 的思路。
這種實現是最基礎的,但是也是問題最多的
1.DelayQueue 的最大容量是有上限的,承受不住過多的訂單
2.每次來新的訂單都要進行排序插入合適位置,訂單量級過大時性能會很低
3.這種方案只適合單機,無法橫向進行分布式擴展
升級版
針對上述的問題,我們采用Redis集群來替代DelayQueue 的設計
1.生成訂單后,立即往redis群集寫訂單信息(信息包含下單時間)。
2.根據redis集群的結點數量,開啟相應倍數(大于等于1)的線程數,n個線程掃描一個結點
3.各線程每次掃描得到的都是下單時間等于60s的訂單,再對這些訂單發送短信,并相應的從redis移除
這種方法解決了基礎版的三個問題,在升級版里還有一些點可以進行細化優化
1.訂單量爆發時,可以將每個訂單直接扔進redis,將壓力分給集群
2.訂單流量中低時,可以利用redis的SortedSet(有序集合)來進行操作,增大redis的掃描線程的顆粒度,進而提升處理效率
3.Redis并發時的數據一致性問題,可以通過redis事務靈活解決
到這里基本就可以解決本文所說的高并發量的帶時間延遲的問題了,我們再深入想一想這種設計的問題,如果時間不固定跨度廣的情況下其實輪詢的方式是不那么理想的會空轉cpu
時間輪(TimingWheel)
Kafka中存在大量的延遲操作,比如延遲生產、延遲拉取以及延遲刪除等。Kafka并沒有使用JDK自帶的Timer或者DelayQueue來實現延遲的功能,而是基于時間輪自定義了一個用于實現延遲功能的定時器(SystemTimer)。
JDK的Timer和DelayQueue和redis的SortedSet插入和刪除操作的平均時間復雜度為O(nlog(n)),并不能滿足Kafka的高性能要求,而基于時間輪可以將插入和刪除操作的時間復雜度都降為O(1)。
時間輪的應用并非Kafka獨有,其應用場景還有很多,在Netty、Akka、Quartz、Zookeeper
等組件中都存在時間輪的蹤影。
參考下圖,Kafka中的時間輪(TimingWheel)是一個存儲定時任務的環形隊列,每個元素可以存放一個定時任務列表(TimerTaskList)。TimerTaskList是一個雙向鏈表,元素是(TimerTaskEntry),其中封裝了真正的定時任務TimerTask。
時間輪由多個時間格組成,每個時間格代表當前時間輪的基本時間跨度
(tickMs)
。時間輪的時間格個數是固定的,可用wheelSize
來表示,那么整個時間輪的總體時間跨度(interval)
可以通過公式 tickMs × wheelSize計算得出。
時間輪還有一個表盤指針(currentTime)
,currentTime
當前指向的時間格也屬于到期部分,表示剛好到期,需要處理此時間格所對應的TimerTaskList
的所有任務。
時間輪就像時鐘一樣,可以通過秒表指向誰就執行誰,當時間跨度大時,可以增加時間輪的級別,如圖
第一層的時間輪tickMs=1ms, wheelSize=20, interval=20ms。
第二層的時間輪的tickMs為第一層時間輪的interval,即為20ms。每一層時間輪的wheelSize是固定的,都是20,那么第二層的時間輪的總體時間跨度interval為400ms。
以此類推,這個400ms也是第三層的tickMs的大小,第三層的時間輪的總體時間跨度為8000ms。
時間輪的優點
1.把任務輪詢的多個線程改裝為了秒針的單一輪詢
2.從毫秒級或者秒級任務獲取執行改裝為批量的范圍獲取
3.擴展性極好,顆粒度可以根據業務場景自適應
4.插入和刪除操作的時間復雜度都降為O(1)