天天看點

使用隊列解耦的架構方案

作者:波斯碼

搞技術的對“高内聚,低耦合”這幾個字應該很熟悉,這是程式設計的一個基本原則,無論對于分布式系統,有幾個子產品的單體程式,以及程式中具體的類、類中的方法,都可以拿來講。這個原則本質上是“分治法”,将一個大問題分解為一個個的小問題,然後各個擊破,整個問題就解決了。相信大家都很明白了,這裡對這個原則就不過多解釋了。

讓我們來看看不使用隊列的情況下如何解耦的:

假設有一個商城系統,業務上劃分為使用者、訂單、财務、消息、倉儲幾個子產品(子產品的劃分實際上也是解耦設計的重要部分,但非這篇文章的關注點),這幾個子產品是分布式部署的,使用者在下單成功以後要做這麼幾件事:通知使用者下單成功、通知倉庫發貨、給财務生成銷售憑證,那麼就要在下單成功的程式邏輯中去調用消息、倉儲、财務子產品的接口。

使用隊列解耦的架構方案

對于一個不經常變動、吞吐量也不是很大的系統,做到這一步也就可以了。

假設商城最近又上線了一個優惠券的功能,需要在下單成功後給使用者發優惠券,這時候怎麼去做呢?一個很直接的想法就是修改下單成功的程式邏輯,增加一個調用發優惠券接口的處理。

使用隊列解耦的架構方案

也能解決問題,但是這時候就要考慮下了,以後還會不會有别的需求?比如下單成功後給使用者增加積分,給推薦的使用者做返利等等。每次都修改下單的程式邏輯其實還是有一定技術風險的,能不能以後不改下單的代碼也能擴充呢?

聰明的你一定想到了辦法,用配置。

使用隊列解耦的架構方案

在訂單模闆中定義一個配置檔案,所有需要下單成功後調用的接口位址都寫到這裡,下單的程式讀取這個配置,一個個去調用。如果以後還有新增的下單後處理,在這裡增加一行配置就行了,不用改下單的代碼。

不過需要注意,每個接口接收的參數應該都是一樣的,或者支援通過參數模闆指派(比如url:http://blog.bossma.cn/notice?orderId={OrderId}&Status={Status},其中{XXX}的内容會被實際值替換,不同的業務可以定義不同的url參數),否則還是要改代碼。

有一天你可能發現下單成功後,也通知使用者了, 也發貨了,但是沒有生成财務憑證,然後到伺服器上翻日志發現下單處理逾時了,調用生成憑證接口沒有成功,至于原因可能是網絡抖動了,也可能是開發人員在更新程式…你想到了分布式事務,不過這個似乎不太好搞。你可能覺得也就是偶爾出現一次,手工處理下就好了。

然後雙十一到了,超過平常10倍的使用者來下單,使用者可能發現送出訂單一直在等待,等待,等…。至于原因也許就是上次發現的逾時問題更嚴重了,本來處理很快的接口調用突然都慢了下來。

這時候你可能需要一個隊列了。

先來看看使用隊列後是什麼樣的?

使用隊列解耦的架構方案

很明顯隻是在下單操作和下單後的操作中間增加了一個隊列,下單成功後訂單資料發送到隊列,通知、發貨、憑證等等操作從隊列接收訂單資料,然後按照自身的業務需求進行處理。

仔細想一下,其實是把上邊提到的配置方式換成了隊列方式,而且它們做的事以及做事方法本質是差不多的,接收資料,然後把資料分發給預先配置好的程式。形式上最大不同是隊列從程序内獨立了出來。

那麼使用隊列帶來了什麼好處呢?

1、更低的耦合,下單操作和後續的通知、發貨、憑證操作完全分開了,下單完畢後發送訂單資料到隊列就像發送一個事件,需要的地方訂閱這個事件就可以了。

2、更好的性能,沒有使用隊列時,下單操作要一次執行下單、通知、發貨、憑證等多個處理,耗時較長;同時可能因為某個調用服務不穩定,導緻整個下單操作不穩定,甚至完全不可用;要對下單操作進行性能優化時,需要考慮的方面過多,不容易達成。

3、容錯,對于瞬時的異常,比如網絡抖動、磁盤IO打滿,導緻後續操作無法執行時,隊列可以緩存這部分資料,直到程式恢複處理能力後繼續處理。沒有使用隊列的時候,隻能記個日志,人工處理。

說幾句廢話,有些坑是使用隊列新引入的,有些坑本來一直就存在,有的坑可以解決,有的坑隻能把危害盡量降低。

如果沒有使用隊列,可以通過本地事務,甚至分布式事務來保證資料的嚴格一緻性。

這不能算一個坑,但是需要了解使用了隊列後就是選擇了最終一緻性,盡管有些隊列支援RPC調用,但本質上仍是最終一緻性。

通知可能延遲了2秒,發貨可能推遲了1分鐘,憑證可能晚生成了10秒,這些應該都是可以接受的,因為對于使用者最重要的下單成功了,至于後邊相對不那麼緊急的事慢慢搞就好了,當然也不能慢的超出人的正常認知,響應速度取決于這些操作的處理能力。

為了防止資料在發送隊列時丢失而生産者卻不知情的情況,很多的隊列都提供了發送确認,隻有發送者收到了發送确認,消息才算投遞成功。

但丢失消息的情況不止這一種,假設隊列服務正常,在下單完成,發送訂單資料到隊列之前,伺服器斷電了,消息就永遠不可能發到隊列了。

使用隊列解耦的架構方案

為了處理此類極端情況,可以采用的方案也有幾個,比如:

将消息和下單放到一個資料庫事務中,即使當時沒能發送到隊列,也能在檢查未發送消息的時候補上這一條。

在所有事務執行前記錄日志,在每個事務完成後記錄日志,從故障恢複後檢查未完成的事務,執行這些事務。

不過除非逼不得已,波斯碼仍然不建議在系統開發之初就搞這個方案,複雜了。

由于網絡問題或者因程式内部異常中斷,發送者不能确定消息是否發送成功時,可能就會再次發送。

如果業務嚴格限制資料隻能處理一次,消費者應該有能力來處理這種重複,可能的解決方案:在資料表中增加一個已處理消息的辨別,或者緩存最近處理過的消息進行判重。

在不使用隊列,多個操作在同一個程序内執行的情況下,不同的接口可能設計了不同的參數,程式編寫者需要在調用接口時傳遞不同的資料,以滿足接口的業務需求。這種慣性思維可能被帶入使用隊列的情況下,為不同的業務發送不同的資料到隊列,消費者消費各自定制的資料。這種做法完全忽視了使用隊列進行解耦的好處。

應該把發送到隊列的資料看作一個消息、或者一個事件,而不是某個具體業務方需要的某幾個資料,這個消息可能是和業務方需求的資料完全吻合,也可能少或者多,對于業務方需要的缺少的資料應該可以根據消息中某個辨別去查詢,這樣才算比較合适的解耦。

比如例子中發送下單成功的通知,需要訂單金額和使用者手機号,從隊列接收到的是訂單資料,其中有訂單金額,沒有手機号,但是有使用者Id,程式需要根據使用者Id去查詢使用者的手機号。

在這篇文章舉的例子中可以使用廣播或者主題的分發方式,一條消息分發到多個消費者隊列,每個消費者消費的消息互相之間沒有影響。

在使用隊列時需要特别關注分發方式,避免消息發送到了不需要的消費者隊列,導緻消費者因無法處理而崩潰;或者不同的業務消費同一個消費者隊列,導緻消息丢失業務處理。

每種隊列産品都提供了高可用的解決方案,我們一般都會在生産環境采用高可用部署。

在實施高可用方案時應該清醒的認識到,可用性越高,就要在性能或一緻性上有些損失,需要按照業務需求平衡這些名額。

市面上常見的隊列也不少,RabbitMQ、RocketMQ、Kafka、ActiveMQ、MetaMQ,甚至Redis也可以幹這件事。網上有大量的文章介紹他們的原理和使用,這裡也不過多的進行說明了。

說一下波斯碼認為的主要三個:RabittMQ、RocketMQ、Kafka。

RabittMQ 社群活躍、管理界面易用、各種開發語言支援的比較好,單機萬級别并發,适合中小型公司。

Kafka 為處理日志而生,吞吐量單機十萬級,社群也很活躍。

RocketMQ 基于Kafka衍生而來,既保持了原有的高并發支援,又在可靠性、穩定性上得到了加持。阿裡開源,社群活躍度一般,适合大公司。