您如何設定一個在買家下訂單後的”第60秒“發簡訊通知賣家發貨,您需要考慮的是 像淘寶一樣的大併發量的訂單。
問題描述:讓您做一個電商平臺,您如何設定一個在買家下訂單後的”第60秒“發簡訊通知賣家發貨,您需要考慮的是 像淘寶一樣的大併發量的訂單。
1、具有排序功能的佇列
2、Redis+定時器
思路 1
原理:第一種思路是延遲佇列實現的原理,其就是一個按時間排好序的佇列,每次put的時候排序,然後take的時候就計算時間是否過期,如果過期則返回佇列第一個元素,否則當前執行緒阻塞X秒,這個也是JDK 自帶 DelayQueue 的思路。
思路 2
原理:第二種思路需要利用Redis的有序集合Sorted Set
業務場景:按京東一天500萬的成交量,一天主要成交時間為8小時,計算得出每秒173個訂單,當然實際上訂單不能均勻分佈在每秒,但我們主要為了論證思想的可行性。
程式碼實現:這裡首先簡單的利用Spring Scheduled作為訂單的生產者,每一秒製造170個訂單,放入Redis,注意Score的生成,為當前時間的後60秒,removeMillis()生成去掉毫秒的時間戳作為Rredis的Zadd方法的 Score。
第二步:同樣利用Spring Scheduled 一秒鐘心跳一次,每次利用當前時間作為Key 從Redis 取資料。
經過測試,沒有出現漏單的情況,這只是簡單的實現,很多地方可以優化,在實際中用也可能會出現很多問題,需要不斷完善,此案例只是提供思路,另外我覺得JDK的 DelayQueue 相對於Redis來說沒有那麼好,因為Queue畢竟每次取一個,如果同一時間的比較多可能不能符合當前這種時間嚴謹的需求。
以上是原作者的回答。
關於第二種思路我們再補充一下:
Sorted Set可以把任務的描述序列化成字串,放在Sorted Set的value中,然後把任務的執行時間戳作為score,利用Sorted Set天然的排序特性,執行時刻越早的會排在越前面。這樣一來,我們只要開一個或多個定時執行緒,每隔一段時間去查一下這個Sorted Set中score小於或等於當前時間戳的元素(這可以通過zrangebyscore
關於這個問題我們再深入思考一下,感興趣的可以留言。這兩個方案更多是偏單機的,如果在分散式環境下,又該如何實現?
思路 3
方案:RabbitMq延遲佇列
原理:RabbitMQ本身沒有直接支援延遲佇列功能,但是我們可以根據其特性Per-Queue Message TTL和 Dead Letter Exchanges實現延時佇列。也可以通過改特性設定訊息的優先順序。
-
特性1、Time To Live(TTL)
RabbitMQ可以針對Queue設定x-expires 或者 針對Message設定 x-message-ttl,來控制訊息的生存時間,如果超時(兩者同時設定以最先到期的時間為準),則訊息變為dead letter(死信)
RabbitMQ針對佇列中的訊息過期時間有兩種方法可以設定。
A: 通過佇列屬性設定,佇列中所有訊息都有相同的過期時間。
B: 對訊息進行單獨設定,每條訊息TTL可以不同。
如果同時使用,則訊息的過期時間以兩者之間TTL較小的那個數值為準。訊息在佇列的生存時間一旦超過設定的TTL值,就成為dead letter
-
特性2、Dead Letter Exchanges(DLX)
RabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可選)兩個引數,如果佇列內出現了dead letter,則按照這兩個引數重新路由轉發到指定的佇列。
x-dead-letter-exchange:出現dead letter之後將dead letter重新發送到指定exchange
x-dead-letter-routing-key:出現dead letter之後將dead letter重新按照指定的routing-key傳送
隊列出現dead letter的情況有:
訊息或者佇列的TTL過期
佇列達到最大長度
訊息被消費端拒絕(basic.reject or basic.nack)並且requeue=false
綜合上述兩個特性,設定了TTL規則之後當訊息在一個佇列中變成死信時,利用DLX特性它能被重新轉發到另一個Exchange或者Routing Key,這時候訊息就可以重新被消費了。
實現延遲佇列方案1
延遲任務通過訊息的TTL和Dead Letter Exchange來實現。我們需要建立2個佇列,一個用於傳送訊息,一個用於訊息過期後的轉發目標佇列。
生產者輸出訊息到Queue1,並且這個訊息是設定有有效時間的,比如3分鐘。訊息會在Queue1中等待3分鐘,如果沒有消費者收掉的話,它就是被轉發到Queue2,Queue2有消費者,收到,處理延遲任務。
該方法主要有三步:
第一步:設定TTL產生死信,建立一個佇列,佇列的訊息過期時間為N分鐘(這個佇列N分鐘內沒有消費者消費訊息則刪除,刪除後佇列內的訊息變為死信)
第二步:設定死信的轉發規則(如果沒有任何規則,則直接丟棄死信)
第三步:配置延時路由規則,需要延時的訊息到exchange後先路由到指定的延時佇列
實現延遲佇列方案2
在rabbitmq 3.5.7及以上的版本提供了一個外掛(rabbitmq-delayed-message-exchange)來實現延遲佇列功能。同時外掛依賴Erlang/OPT 18.0及以上。
但是rabbitmq像淘寶那樣的量每天流轉幾千億條訊息,雙十一大促,是搞不定阿里的問題的。
思路 4
方案:時間輪(TimingWheel)& 層級時間輪
原理:該方案的靈感來自於Kafka,JDK的Timer和DelayQueue插入和刪除操作的平均時間複雜度為O(nlog(n)),Kafka基於時間輪可以將插入和刪除操作的時間複雜度都降為O(1)。Kafka的原理請參照《Rabbitmq實戰》作者朱忠華老先生的Kafka解惑之時間輪(TimingWheel)。
時間輪分為單級時間輪和層級時間輪。
時間輪簡介:時間輪方案將現實生活中的時鐘概念引入到軟體設計中,主要思路是定義一個時鐘週期(比如時鐘的12小時)和步長(比如時鐘的一秒走一次),當指標每走一步的時候,會獲取當前時鐘刻度上掛載的任務並執行:
1.單層時間輪設計:
以上圖為例,假設一個格子為1秒,整個一圈表示的時間為12秒,此時需要新增5秒後執行的任務,則此時改任務一個放到第(1+5=6)的格子內,如果此時新增13秒後執行任務,此時該任務應該等轉完一圈後round為1 放到第二格子中,指標每轉動一個一格,獲取當前round為0的任務執行,格子上的其他任務round減1
問題: 當時間跨度很大,數量很大時,單層的時間輪造成的round很大,一個格子中鏈很長,所以衍生出多級時間輪的設計方案
2.多級時間輪設計方案:
最小輪子走一圈,它的上層輪子走一格
假設圖中每層輪子為20個格子,第一層輪子最小時間間隔為1ms,第二層為20ms,第三層為400ms,此時新增5ms後執行的任務,此時應該新增到第一層的第5格子中。如果此時新增445ms後執行的任務,則第一層表示的時間跨度不夠,第二層表示的時間跨度也不夠,第三層表示的時間跨度足夠,該任務應該放到第三層輪子第二格子中,該輪子指標指到第二格子中時,計算離任務啟動時間還有多長時間,慢慢將該任務移動到底層輪子上,最終任務到期執行。
關於更多如何在MQ中實現支援任意延遲的訊息?建議看一下這篇文章https://www.cnblogs.com/hzmark/p/mq-delay-msg.html,需要說明的是
阿里雲上對業界MQ功能的對比,其中開源產品中只有阿里的RocketMQ支援延遲訊息,且是固定的18個Level。固定Level的含義是延遲是特定級別的,比如支援3秒、5秒的Level,那麼使用者只能傳送3秒延遲或者5秒延遲,不能傳送8秒延遲的訊息。訊息佇列RocketMQ的阿里雲版本(收費版本)才支援到精確到秒級別的延遲訊息(沒有特定Level的限制)。
對支援任意延遲的需求確實不強,因為:
-
延遲並不是MQ場景的核心功能,業務單獨做一個替代方案的成本不大
-
業務上一般對延遲的需求都是固定的,比如下單後半小時check是否付款,發貨後7天check是否收貨
支援任意延遲意味著訊息是需要在服務端進行排序的。如何處理排序和訊息儲存,但是如何更牛逼的進行任意級別的延遲,進行海量的資料落盤呢?我們來看思路5。
思路 5
方案:單層檔案時間輪
原理:
該圖是開源版本RocketMQ支援18個Level的方案簡圖
結合RocketMQ的做法,但是又不同於它。
我瞎想(趕緊留言噴)一下(後面有高手要發系統性的文章,我拋磚引玉),由於大量堆積一定要1⃣️落盤,另外結合一下rabbit的2⃣️延時佇列+Kafka的3⃣️TimingWheel,來打造一個支援任意級別的延遲的工具。
第一步,CommitLog需要區分是否是延遲,而非延遲進入正常消費佇列。
第二步,延遲的CommitLog剝離出來,按照訊息順序落盤,由於面對海量資料,需要進行落盤和訊息備份,這裡可以和流式計算Jstorm合作提升效能
第三步,TimeWheel載入延遲時間臨近的訊息到記憶體進行處理
思路 5
方案:其他
好了,讓我們看看其他網友針對這個問題的看法: