消息隊列是最古老的中間件之一,從系統之間有通信需求開始,就自然產生了消息隊列。但是給消息隊列下一個准確的定義卻不太容易。我們知道,消息隊列的主要功能就是收發消息,但是它的作用不僅僅只是解決應用之間的通信問題這么簡單。
我們舉個例子說明一下消息隊列的作用。話說小袁是一家巧克力作坊的老板,生產出美味的巧克力需要三道工序:首先將可可豆磨成可可粉,然后將可可粉加熱並加入糖變成巧克力漿,最后將巧克力漿灌入模具,撒上堅果碎,冷卻后就是成品巧克力了。
最開始的時候,每次研磨出一桶可可粉后,工人就會把這桶可可粉送到加工巧克力漿的工人手上,然后再回來加工下一桶可可粉。小袁很快就發現,其實工人可以不用自己運送半成品,於是他在每道工序之間都增加了一組傳送帶,研磨工人只要把研磨好的可可粉放到傳送帶上,就可以去加工下一桶可可粉了。 傳送帶解決了上下游工序之間的“通信”問題。
傳送帶上線后確實提高了生產效率,但也帶來了新的問題:每道工序的生產速度並不相同。在巧克力漿車間,一桶可可粉傳送過來時,工人可能正在加工上一批可可粉,沒有時間接收。不同工序的工人們必須協調好什么時間往傳送帶上放置半成品,如果出現上下游工序加工速度不一致的情況,上下游工人之間必須互相等待,確保不會出現傳送帶上的半成品無人接收的情況。
為了解決這個問題,小袁在每組傳送的下游帶配備了一個暫存半成品的倉庫,這樣上游工人就不用等待下游工人有空,任何時間都可以把加工完成的半成品丟到傳送帶上,無法接收的貨物被暫存在倉庫中,下游工人可以隨時來取。傳送帶配備的倉庫實際上起到了“通信”過程中“緩存”的作用。
傳送帶解決了半成品運輸問題,倉庫可以暫存一些半成品,解決了上下游生產速度不一致的問題,小袁在不知不覺中實現了一個巧克力工廠版的消息隊列。
哪些問題適合使用消息隊列來解決?
接下來我們說一下日常開發中,哪些問題適合使用消息隊列解決。
1. 異步處理
大多數程序員在面試中,應該都問過或被問過一個經典卻沒有標准答案的問題:如何設計一個秒殺系統?這個問題可以有一百個版本的合理答案,但大多數答案中都離不開消息隊列。
秒殺系統需要解決的核心問題是,如何利用有限的服務器資源,盡可能多地處理短時間內的海量請求。我們知道,處理一個秒殺請求包含了很多步驟,例如:
- 風險控制;
- 庫存鎖定;
- 生成訂單;
- 短信通知;
- 更新統計數據。
如果沒有任何優化,正常的處理流程是:App 將請求發送給網關,依次調用上述 5 個流程,然后將結果返回給 APP。
對於對於這 5 個步驟來說,能否決定秒殺成功,實際上只有風險控制和庫存鎖定這 2 個步驟。只要用戶的秒殺請求通過風險控制,並在服務端完成庫存鎖定,就可以給用戶返回秒殺結果了,對於后續的生成訂單、短信通知和更新統計數據等步驟,並不一定要在秒殺請求中處理完成。
所以當服務端完成前面 2 個步驟,確定本次請求的秒殺結果后,就可以馬上給用戶返回響應,然后把請求的數據放入消息隊列中,由消息隊列異步地進行后續的操作。
處理一個秒殺請求,從 5 個步驟減少為 2 個步驟,這樣不僅響應速度更快,並且在秒殺期間,我們可以把大量的服務器資源用來處理秒殺請求。秒殺結束后再把資源用於處理后面的步驟,充分利用有限的服務器資源處理更多的秒殺請求。
可以看到,在這個場景中,消息隊列被用於實現服務的異步處理。這樣做的好處是:
- 可以更快地返回結果;
- 減少等待,自然實現了步驟之間的並發,提升系統總體的性能。
2. 流量控制
繼續說我們的秒殺系統,我們已經使用消息隊列實現了部分工作的異步處理,但我們還面臨一個問題:如何避免過多的請求壓垮我們的秒殺系統?
一個設計健壯的程序有自我保護的能力,也就是說,它應該可以在海量的請求下,還能在自身能力范圍內盡可能多地處理請求,拒絕處理不了的請求並且保證自身運行正常。不幸的是,現實中很多程序並沒有那么“健壯”,而直接拒絕請求返回錯誤對於用戶來說也是不怎么好的體驗。
因此,我們需要設計一套足夠健壯的架構來將后端的服務保護起來。我們的設計思路是,使用消息隊列隔離網關和后端服務,以達到流量控制和保護后端服務的目的。
加入消息隊列后,整個秒殺流程變為:
- 網關在收到請求后,將請求放入請求消息隊列;
- 后端服務從請求消息隊列中獲取 APP 請求,完成后續秒殺處理過程,然后返回結果。
秒殺開始后,當短時間內大量的秒殺請求到達網關時,不會直接沖擊到后端的秒殺服務,而是先堆積在消息隊列中,后端服務按照自己的最大處理能力,從消息隊列中消費請求進行處理。
對於超時的請求可以直接丟棄,APP 將超時無響應的請求處理為秒殺失敗即可。運維人員還可以隨時增加秒殺服務的實例數量進行水平擴容,而不用對系統的其他部分做任何更改。
這種設計的優點是:能根據下游的處理能力自動調節流量,達到“削峰填谷”的作用。但這樣做同樣是有代價的:
- 增加了系統調用鏈環節,導致總體的響應時延變長。
- 上下游系統都要將同步調用改為異步消息,增加了系統的復雜度。
那還有沒有更簡單一點兒的流量控制方法呢?如果我們能預估出秒殺服務的處理能力,就可以用消息隊列實現一個令牌桶,更簡單地進行流量控制。
令牌桶控制流量的原理是:單位時間內只發放固定數量的令牌到令牌桶中,規定服務在處理請求之前必須先從令牌桶中拿出一個令牌,如果令牌桶中沒有令牌,則拒絕請求。這樣就保證單位時間內,能處理的請求不超過發放令牌的數量,起到了流量控制的作用。
實現的方式也很簡單,不需要破壞原有的調用鏈,只要網關在處理 APP 請求時增加一個獲取令牌的邏輯。
令牌桶可以簡單地用一個有固定容量的消息隊列加一個“令牌發生器”來實現:令牌發生器按照預估的處理能力,勻速生產令牌並放入令牌隊列(如果隊列滿了則丟棄令牌),網關在收到請求時去令牌隊列消費一個令牌,獲取到令牌則繼續調用后端秒殺服務,如果獲取不到令牌則直接返回秒殺失敗。
以上是常用的使用消息隊列兩種進行流量控制的設計方法,你可以根據各自的優缺點和不同的適用場景進行合理選擇。
3. 服務解耦
消息隊列的另外一個作用,就是實現系統應用之間的解耦。再舉一個電商的例子來說明解耦的作用和必要性。
我們知道訂單是電商系統中比較核心的數據,當一個新訂單創建時:
- 支付系統需要發起支付流程;
- 風控系統需要審核訂單的合法性;
- 客服系統需要給用戶發短信告知用戶;
- 經營分析系統需要更新統計數據;
- ……
這些訂單下游的系統都需要實時獲得訂單數據。隨着業務不斷發展,這些訂單下游系統不斷的增加,不斷變化,並且每個系統可能只需要訂單數據的一個子集,負責訂單服務的開發團隊不得不花費很大的精力,應對不斷增加變化的下游系統,不停地修改調試訂單系統與這些下游系統的接口。任何一個下游系統接口變更,都需要訂單模塊重新進行一次上線,對於一個電商的核心服務來說,這幾乎是不可接受的。
所有的電商都選擇用消息隊列來解決類似的系統耦合過於緊密的問題。引入消息隊列后,訂單服務在訂單變化時發送一條消息到消息隊列的一個主題 Order 中,所有下游系統都訂閱主題 Order,這樣每個下游系統都可以獲得一份實時完整的訂單數據。
無論增加、減少下游系統或是下游系統需求如何變化,訂單服務都無需做任何更改,實現了訂單服務與下游服務的解耦。
小結
以上就是消息隊列最常被使用的三種場景:異步處理、流量控制和服務解耦。當然,消息隊列的適用范圍不僅僅局限於這些場景,還有包括:
- 作為發布 / 訂閱系統實現一個微服務級系統間的觀察者模式;
- 連接流計算任務和數據;
- 用於將消息廣播給大量接收者。
簡單的說,我們在單體應用里面需要用隊列解決的問題,在分布式系統中大多都可以用消息隊列來解決。
同時我們也要認識到,消息隊列也有它自身的一些問題和局限性,包括:
- 引入消息隊列帶來的延遲問題;
- 增加了系統的復雜度;
- 可能產生數據不一致的問題。
所以我們說沒有最好的架構,只有最適合的架構,根據目標業務的特點和自身條件選擇合適的架構,才是體現一個架構師功力的地方。
思考題
在你工作或學習涉及到的系統中,哪些問題可以通過引入消息隊列來解決?對於系統中已經使用消息隊列,可以對應到這一講中提到的哪個場景?如果沒有可以對應的場景,那這個消息隊列在系統中起到的是什么作用?解決了什么問題?是否又帶來了什么新的問題?歡迎在留言區寫下你的想法。