訊息佇列(基礎篇)-1 為什麼需要訊息佇列?
解決實際工作中的問題
非同步處理
大多數程式設計師在面試中,應該都問過或被問過一個經典卻沒有標準答案的問題:如何設計一個秒殺系統?這個問題可以有一百個版本的合理答案,但大多數答案中都離不開訊息佇列。
秒殺系統需要解決的核心問題是,如何利用有限的伺服器資源,儘可能多地處理短時間內的海量請求。我們知道,處理一個秒殺請求包含了很多步驟,例如:
- 風險控制;
- 庫存鎖定;
- 生成訂單;
- 簡訊通知;
- 更新統計資料。
如果沒有任何優化,正常的處理流程是:App 將請求傳送給閘道器,依次呼叫上述 5 個流程,然後將結果返回給 APP。
對於對於這 5 個步驟來說,能否決定秒殺成功,實際上只有風險控制和庫存鎖定這 2 個步驟。只要使用者的秒殺請求通過風險控制,並在服務端完成庫存鎖定,就可以給使用者返回秒殺結果了,對於後續的生成訂單、簡訊通知和更新統計資料等步驟,並不一定要在秒殺請求中處理完成。
所以當服務端完成前面 2 個步驟,確定本次請求的秒殺結果後,就可以馬上給使用者返回響應,然後把請求的資料放入訊息佇列中,由訊息佇列非同步地進行後續的操作。
處理一個秒殺請求,從 5 個步驟減少為 2 個步驟,這樣不僅響應速度更快,並且在秒殺期間,我們可以把大量的伺服器資源用來處理秒殺請求。秒殺結束後再把資源用於處理後面的步驟,充分利用有限的伺服器資源處理更多的秒殺請求。
可以看到,在這個場景中,訊息佇列被用於實現服務的非同步處理。這樣做的好處是:
- 可以更快地返回結果;
- 減少等待,自然實現了步驟之間的併發,提升系統總體的效能。
流量控制
繼續說我們的秒殺系統,我們已經使用訊息佇列實現了部分工作的非同步處理,但我們還面臨一個問題:如何避免過多的請求壓垮我們的秒殺系統?
一個設計健壯的程式有自我保護的能力,也就是說,它應該可以在海量的請求下,還能在自身能力範圍內儘可能多地處理請求,拒絕處理不了的請求並且保證自身執行正常。不幸的是,現實中很多程式並沒有那麼“健壯”,而直接拒絕請求返回錯誤對於使用者來說也是不怎麼好的體驗。
因此,我們需要設計一套足夠健壯的架構來將後端的服務保護起來。我們的設計思路是,使用訊息佇列隔離閘道器和後端服務,以達到流量控制和保護後端服務的目的。
加入訊息佇列後,整個秒殺流程變為:
- 閘道器在收到請求後,將請求放入請求訊息佇列;
- 後端服務從請求訊息佇列中獲取 APP 請求,完成後續秒殺處理過程,然後返回結果。
秒殺開始後,當短時間內大量的秒殺請求到達閘道器時,不會直接衝擊到後端的秒殺服務,而是先堆積在訊息佇列中,後端服務按照自己的最大處理能力,從訊息佇列中消費請求進行處理。
對於超時的請求可以直接丟棄,APP 將超時無響應的請求處理為秒殺失敗即可。運維人員還可以隨時增加秒殺服務的例項數量進行水平擴容,而不用對系統的其他部分做任何更改。
這種設計的優點是:能根據下游的處理能力自動調節流量,達到“削峰填谷”的作用。但這樣做同樣是有代價的:
- 增加了系統呼叫鏈環節,導致總體的響應時延變長。
- 上下游系統都要將同步呼叫改為非同步訊息,增加了系統的複雜度。
那還有沒有更簡單一點兒的流量控制方法呢?如果我們能預估出秒殺服務的處理能力,就可以用訊息佇列實現一個令牌桶,更簡單地進行流量控制。
令牌桶控制流量的原理是:單位時間內只發放固定數量的令牌到令牌桶中,規定服務在處理請求之前必須先從令牌桶中拿出一個令牌,如果令牌桶中沒有令牌,則拒絕請求。這樣就保證單位時間內,能處理的請求不超過發放令牌的數量,起到了流量控制的作用。
實現的方式也很簡單,不需要破壞原有的呼叫鏈,只要閘道器在處理 APP 請求時增加一個獲取令牌的邏輯。
令牌桶可以簡單地用一個有固定容量的訊息佇列加一個“令牌發生器”來實現:令牌發生器按照預估的處理能力,勻速生產令牌並放入令牌佇列(如果佇列滿了則丟棄令牌),閘道器在收到請求時去令牌佇列消費一個令牌,獲取到令牌則繼續呼叫後端秒殺服務,如果獲取不到令牌則直接返回秒殺失敗。
以上是常用的使用訊息佇列兩種進行流量控制的設計方法,你可以根據各自的優缺點和不同的適用場景進行合理選擇。
服務解耦
訊息佇列的另外一個作用,就是實現系統應用之間的解耦。再舉一個電商的例子來說明解耦的作用和必要性。
我們知道訂單是電商系統中比較核心的資料,當一個新訂單建立時:
- 支付系統需要發起支付流程;
- 風控系統需要稽核訂單的合法性;
- 客服系統需要給使用者發簡訊告知使用者;
- 經營分析系統需要更新統計資料;
- ……
這些訂單下游的系統都需要實時獲得訂單資料。隨著業務不斷髮展,這些訂單下游系統不斷的增加,不斷變化,並且每個系統可能只需要訂單資料的一個子集,負責訂單服務的開發團隊不得不花費很大的精力,應對不斷增加變化的下游系統,不停地修改除錯訂單系統與這些下游系統的介面。任何一個下游系統介面變更,都需要訂單模組重新進行一次上線,對於一個電商的核心服務來說,這幾乎是不可接受的。
所有的電商都選擇用訊息佇列來解決類似的系統耦合過於緊密的問題。引入訊息佇列後,訂單服務在訂單變化時傳送一條訊息到訊息佇列的一個主題 Order 中,所有下游系統都訂閱主題 Order,這樣每個下游系統都可以獲得一份實時完整的訂單資料。
無論增加、減少下游系統或是下游系統需求如何變化,訂單服務都無需做任何更改,實現了訂單服務與下游服務的解耦。
小結
以上就是訊息佇列最常被使用的三種場景:非同步處理、流量控制和服務解耦。當然,訊息佇列的適用範圍不僅僅侷限於這些場景,還有包括:
- 作為釋出 / 訂閱系統實現一個微服務級系統間的觀察者模式;
- 連線流計算任務和資料;
- 用於將訊息廣播給大量接收者。
簡單的說,我們在單體應用裡面需要用佇列解決的問題,在分散式系統中大多都可以用訊息佇列來解決。
同時我們也要認識到,訊息佇列也有它自身的一些問題和侷限性,包括:
- 引入訊息佇列帶來的延遲問題;
- 增加了系統的複雜度;
- 可能產生資料不一致的問題。