阿里面試中遇到的一些架構問題

java填坑路發表於2018-09-11

一位網友之前面試淘點點的時候被問倒得一個問題至今牽掛,工作兩年,由於工作環境的限制,沒能接觸到一些大資料量的併發工作,也沒能有機遇參與複雜系統的設計,而學習複雜或高併發系統的唯一途徑就是閱讀原始碼,慚愧的是,至今也只閱讀了Tomcat的部分原始碼,於是他在oschina上貼出問題與網際網路猿一同分析。

http://www.oschina.net/question/926166_2137672

問題描述:讓您做一個電商平臺,您如何設定一個在買家下訂單後的”第60秒“發簡訊通知賣家發貨,您需要考慮的是 像淘寶一樣的大併發量的訂單。

1、具有排序功能的佇列

2、Redis+定時器

思路 1

原理:第一種思路是延遲佇列實現的原理,其就是一個按時間排好序的佇列,每次put的時候排序,然後take的時候就計算時間是否過期,如果過期則返回佇列第一個元素,否則當前執行緒阻塞X秒,這個也是JDK 自帶 DelayQueue 的思路。

思路 2

原理:第二種思路(來自java架構沉思錄)需要利用Redis的有序集合Sorted Set,說到使用 Redis 就不得不考慮Score的設計,因為它直接決定你程式碼的複雜度,通過精確到秒的時間做Score(去掉毫秒),然後使用執行緒每一秒掃一次,使用當前時間作為zrangeBysocre命令的Score去查詢。

業務場景:按京東一天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命令實現),然後再執行元素對應的任務即可。當然,執行完任務後,還要將元素從Sorted Set中刪除,避免任務重複執行。如果是多個執行緒去輪詢這個Sorted Set,還有考慮併發問題,假如說一個任務到期了,也被多個執行緒拿到了,這個時候必須保證只有一個執行緒能執行這個任務,這可以通過zrem命令來實現,只有刪除成功了,才能執行任務,這樣就能保證任務不被多個任務重複執行了。

關於這個問題我們再深入思考一下,這兩個方案更多是偏單機的,如果在分散式環境下,又該如何實現?

思路 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的做法,但是又不同於它。

我瞎想一下(後面有高手要發系統性的文章,我拋磚引玉),由於大量堆積一定要落盤,另外結合一下rabbit的延時佇列+Kafka的TimingWheel,來打造一個支援任意級別的延遲的工具。

第一步,CommitLog需要區分是否是延遲,而非延遲進入正常消費佇列。

第二步,延遲的CommitLog剝離出來,按照訊息順序落盤,由於面對海量資料,需要進行落盤和訊息備份,這裡可以和流式計算Jstorm合作提升效能

第三步,TimeWheel載入延遲時間臨近的訊息到記憶體進行處理

歡迎工作一到五年的Java工程師朋友們加入Java架構開發:744677563

群內提供免費的Java架構學習資料(裡面有高可用、高併發、高效能及分散式、Jvm效能調優、Spring原始碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用”沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!


相關文章