消息隊列高手課筆記

爲什麼要使用消息隊列?消息隊列的好處?用途?

消息隊列用來解決系統之間的通信問題,主要功能就是收發消息,使用消息隊列有幾個好處:
①解耦:減少了系統之間的依賴關係,如果在一個系統A中直接調用另一個系統B的接口,如果B執行失敗,A也會執行失敗,兩個系統高度耦合,不利於代碼的擴展和維護。
②異步:提高系統響應速度,減少用戶的等待時間。舉例:假設我現在有一個下單功能,它包含了鎖定庫存,訂單入庫,給用戶發短信3個操作,需要等這3個操作完成後才能給用戶返回結果,用戶需要等待較長的時間。我可以這樣優化,在我這個下單功能中決定下單是否成功的操作是鎖定庫存,等鎖定庫存操作完成後就立刻返回結果給用戶,然後將請求的數據放到消息隊列中,由消息隊列異步進行後續2步操作。好處是減少了用戶等待的時間,充分利用了服務器的資源在短時間內處理了更多的請求。
③削峯限流:比如說秒殺系統秒殺時一秒鐘有5000併發量,但是服務器每秒鐘只能請求1000併發量,那多出來的4000個請求,可能會使系統崩潰,解決辦法是把多出來的請求到消息隊列裏中,秒殺系統根據自己能夠處理的請求數去消息對隊列裏拿數據。這樣系統就不會崩潰。缺點是增加了系統調用鏈環節,導致總體響應時延變長。另外上下游系統需要將同步調用改爲異步消息,增加了系統的複雜度。

使用消息隊列有什麼缺點?

①消息隊列時延問題
②降低系統的可用性:系統引入的外部依賴越多,越容易掛掉。
③系統複雜度提高:使用 MQ 後可能需要保證消息沒有被重複消費、消息沒有丟失、保證消息傳遞的順序性 等問題;
④一致性問題:A 系統處理完直接返回成功了,通過消息隊列通知BCD執行,而且 B 和 D 兩個系統也寫庫成功了,但 C 系統寫庫失敗了,就造成數據不一致了。

怎麼解決這些問題呢?

你還了解哪些消息隊列?

ActiveMQ:早期用的比較多,現在基本上沒什麼人用,社區也不是很活躍。吞吐量是萬級,時延ms級別,有較低的概率會丟失消息。
RabbitMQ:使用簡單,開箱即用。性能也很好,社區活躍度高,吞吐量萬級,是用erlang語言開發的,對於java開發者來說很難看懂,所以很難做擴展和二次開發,對於消息堆積的支持不是很好,消息堆積時,性能會急劇下降。如果對性能要求不高,用RabbitMQ即可。
RocketMQ:吞吐量很高,達到了幾十萬級別,經過配置,消息可以做到0丟失,用java語言開發的,比較容易做擴展或二次開發,是目前比較多的消息隊列。
Kafka:吞吐量很高,達到了幾十萬級別,也可以做到消息0丟失,功能簡單,一般用於大數據領域做實時計算和日誌採集。
爲什麼你這兩個項目分別使用了RocketMQ和Kafka?
RocketMQ它的吞吐量很高,時延低,穩定性高。適合處理在線業務,比如在交易系統中傳遞訂單。所以我在秒殺系統中使用了它。
而Kafka採用了異步批量設計,這種設計使得它的性能很高,但是帶來的問題是它的響應時延比較高,當客戶端發送一條消息時,Kafka 並不會立即發送出去,而是攢一批了再發送,所以 Kafka 不太適合在線業務的場景。在我的討論社區項目中,如果有點贊,評論或者關注等操作,我需要去異步進行消息通知,對實時性要求不高,所以Kafka比較合適。

消息隊列模型:

①隊列模型(點對點):生產者-消費者,一條消息只能被一個消費者消費到
②發佈訂閱模型:發佈者-訂閱者。

RocketMQ模型(Kafka模型也是一樣的)
①RocketMQ中有生產者、消費者、主題等概念,每個主題可以包含多個隊列,通過多個隊列來實現並行生產和消費。這種情況下,只能在隊列上保證消息的有序性,在主題層面無法保證消息順序消費。
②RocketMQ中使一條消息能被多個消費者消費是通過消費組來實現的。在一個消費組中的多個消費者不能消費同一條消息,(同一個組內的消費者是競爭消費關係)但是一條消息可以被多個消費組消費。
③由於消息需要被不同的消費組進行多次消費,所以RocketMQ在隊列上爲每個消費組維護了一個消費位置,每成功消費一條消息,位置就加一。

事務消息,爲什麼消息隊列需要事務?

消息隊列中的事務,主要就是用來解決消息生產者和消息消費者的數據一致性問題。
假設一個下單過程,鎖定庫存成功後發消息到消息隊列,訂單系統收到消息去做訂單入庫操作。那麼會有這兩種可能,庫存鎖定成功,消息發送失敗。庫存鎖定失敗,消息發送成功。這兩種情況都有可能造成數據不一致問題。所以就需要事務來保證這兩個操作要麼全部執行成功,要麼全部失敗。在分佈式系統中要用分佈式事務。

比較常見的分佈式事務實現有2PC(二階段提交)、TCC(Try-Confirm-Cancel)和事務消息。
每一種都有其特定的使用場景。

事務消息使用的場景主要是那些需要異步更新數據,並且對數據實時性要求不太高的場景。
事務消息需要消息隊列提供相應的功能才能實現,具體實現如下:

①首先,訂單系統(生產者)在消息隊列上開啓一個事務,然後訂單系統給消息服務器發送一個“半消息”,這個半消息包含了完整的消息內容,只不過它在事務提交之前,對於消費者是不可見的。
②半消息發送成功後,訂單系統就可以執行本地事務了,然後根據本地事務的執行結果決定提交或者回滾事務消息。如果本地事務執行成功,就提交事務消息,消費者就可以看到這條消息。如果本地事務執行失敗,那就回滾事務消息,消費者就不會收到這條消息。這樣就實現了要麼都成功,要麼都失敗。

如果提交或回滾事務消息時失敗了怎麼辦?
①RocketMQ增加了事務反查機制來解決事務消息提交失敗的問題。如果訂單系統(生產者)在提交或者回滾事務消息時發生網絡異常,導致Broker沒有收到提交或者回滾的請求,Broker會定期去Producer上去反查這個事務對應的本地事務的狀態,然後根據反查結果決定提交或者回滾這個事務。
②爲了支撐這個反查機制,我們的業務代碼需要實現一個反查本地事務狀態的接口,返回本地事務是執行成功還是失敗。

消息隊列是如何保證高可用的?

是通過多副本來實現的,每一個分區都有多個副本,生產者和消費者只和主副本交互,其他副本會同步數據,如果主副本所在的broker掛掉,會選出一個從副本作爲leader,這樣保證了高可用。

檢測消息是否丟失的辦法

你是可以利用消息隊列的有序性來驗證是否有消息丟失。Producer端在發消息時給每個消息附加一個連續遞增的序號,然後在Consumer端來檢查這個序號的連續性來判斷是否有消息丟失,還可以通過缺失的序號來判斷丟的是哪條消息。
但是要注意的是像Kafka和RocketMQ這樣的消息隊列,是不保證在topic上的有序的,只能保證分區上是有序的,所以再發消息時必須指定分區並按區分別編號,並且檢測時也要分區單獨檢測消息的連續性。

如何保證消息不丟失的?如何保證消息隊列的可靠性的?

一條消息從生產到被消費,會經過三個階段:生產階段、存儲階段、消費階段。要從這三個階段保證消息不丟失。
生產階段:Producer創建消息,消息經過網絡傳輸發送到Broker端。
存儲階段:Broker將消息存儲在磁盤中。如果是集羣的話,消息還會被複制到其他節點上。
消費階段:Consumer從Broker上拉取消息,消息經過網絡傳輸發送到Consumer上。

①生產階段:在生產階段,是通過請求確認機制,來保證消息的可靠傳遞的。Producer發送消息給broker,當broker收到後會返回確認信息給Producer。所以生產者通過Broker返回的確認信息,判斷消息在生產階段沒有丟失,如果丟失可以重發。(注意要正確處理返回值或者捕獲異常,同步發送時,只要注意捕獲異常即可;異步發送時,需要在回調方法裏檢查)
②存儲階段:在存儲階段,消息到了Broker端,會優先保存在內存中,然後立刻返回確認信息給生產者。隨後Broker會定期批量地將消息從內存異步刷到磁盤。這種方式減少了IO次數,性能更好,但是如果Broker宕機,消息還未來得及刷入磁盤,就會出現消息丟失。

如果對消息的可靠性要求非常高,可以通過配置Broker參數來避免因爲宕機而丟消息。

  1. 首先需要配置Broker參數flushDiskType=SYNC_FLUSH,意思是將消息保存機制修改爲同步刷盤方式,也就是Broker收到消息後,就將消息存儲到磁盤,存儲成功再給生產者返回確認信息。
  2. (主從部署)爲了保證可用性,Broker通常採用一主多從部署方式,爲了保證消息不丟失,還要將消息複製到slave節點。(默認方式下,消息寫入master成功,就會返回確認信息給生產者,接着異步將消息複製到slave節點)此時如果master突然宕機不可恢復,那麼還未複製到slave的消息就會丟失。爲了防止消息丟失,可以採用同步複製方式,master節點把消息成功複製到slave節點之後,纔會返回確認響應。需要配置參數brokerRole=SYNC_MASTER。並且從節點也要設置爲同步刷盤。

以上配置雖然可以保證消息不丟失,但是會降低性能,生產中要綜合選擇。

③消費階段:消費者從Broker中拉取消息進行消費,消費成功會發送確認響應給Broker,如果Broker未收到消費確認響應,消費者下次還會再次拉取到該消息,進行重試。

以上可以保證消息不丟失,但是可能會重複發送消息,那麼怎麼保證消息不被重複消費呢?

讓消費消息的操作具有冪等性,一個操作具有冪等性是指這個操作任意多次執行所產生的影響與一次執行的影響相同。具體的實現有:

  1. 利用數據庫的唯一約束實現冪等
    每當要消費一條消息時,就先將其插入到數據庫中,由於唯一性約束,相同的消息只會被插入一次。
  2. 利用redis的SETNX命令也可以

消息積壓你是怎麼處理的?

設計系統時,要保證消費端的消費性能要高於生產者的發送性能,這樣系統才能健康持續運行。
消息積壓產生的原因有2種:第一是消息生產速度過快,另一種是消息消費速度變慢。可以通過查看消息隊列內置的監控數據,確定是生產端還是消費端的原因。

如果是消費端的原因,就進行消費端性能優化:
①首先可以優化消費業務邏輯,儘量減少冗餘。還可以增加消費端的併發數,也就是擴容Consumer實例,也必須同步擴容主題中的隊列數量,確保Consumer的實例數量和隊列數量相等。
②如果是有大促或者搶購導致消息突增,短時間內不可能通過優化代碼來解決,唯一的方法是通過增加消費者實例數來提升總體的消費能力。如果短時間內沒有足夠的服務器資源進行擴容,那麼可以將系統降級,關閉一些不重要的業務,減少生產者生產的消息數量,讓系統儘可能正常運轉,服務一些重要的業務。

如果通過監控發現生產消息的速度和消費消息的速度都沒有什麼異常,那就需要檢查一下消費端,是不是消費失敗導致了一條消息反覆消息。

如果監控到消費變慢了,需要檢查一下消費實例,分析一下是什麼原因導致消費變慢。可以檢查一下日誌看是否有大量的消費錯誤,還可以通過打印堆棧信息,看一下消費線程是不是卡在什麼地方了,比如發生死鎖或者等待資源。

你是怎麼保證消息按順序消費?

RocketMQ一個topic可以有多個分區,它可以分區中順序消費,但是不能保證每個topic順序消費,如果想要保證每個topic順序消費的話,那麼就只爲一個topic設置一個分區。
或者發送消息時指定發送的分區。

Kafka怎麼保證不丟消息的?

生產階段:生產者發送消息給Broker後,通過Broker返回的信息來判斷消息發送是否成功,如果失敗的話就重新發送。
①使用send方法後,接着調用get方法獲取調用結果,但是這樣會讓它變成同步操作。(不推薦)
②爲send方法添加回調函數來獲取結果,
要將Producer的retries重試次數設置大一些,可以保證出現網絡問題的話可以自動重發消息,避免消息丟失。

消費階段:在分區中有一個offset的概念,表示消費者消費到的位置,默認配置下,當消費者拉取消息之後,消費者會自動提交offset。但是這樣可能會產生這樣一個問題,消費者剛拿到消息正準備消費時突然掛掉了,消息實際上沒有被消費,但是offset卻被提交了,就會導致消息丟失。
解決辦法是關閉自動提交offset,在真正消費完消息之後再手動提交offset。(可能會帶來重複消費問題)

存儲階段:Kafka爲分區引入了多副本機制,多個副本中有一個leader,其他的都是follower。消息會被先發送到leader副本,然後follower副本才能從leader副本中拉取消息進行同步。生產者和消費者只和leader副本交互,其他副本的存在只是爲了保證消息存儲的安全性。如果leader副本所在的broker掛掉,那麼就要從follower副本中重新選出一個leader,但是如果leader的數據還有一部分沒有被follower副本同步的話,就會造成消息丟失。

解決:
①不要使用 producer.send(msg),而要使用 producer.send(msg, callback)。記住,一 定要使用帶有回調通知的 send 方法。
②設置 retries 爲一個較大的值。這裏的 retries 同樣是 Producer 的參數,對應前面提到 的 Producer 自動重試。當出現網絡的瞬時抖動時,消息發送可能會失敗,此時配置了 retries > 0 的 Producer 能夠自動重試消息發送,避免消息丟失。
③設置 acks = all。acks 是 Producer 的一個參數,代表了你對“已提交”消息的定義。 acks的默認值爲1,爲1代表消息被leader副本接收就算作被成功發送。配置爲all代表所有的副本都接受這個消息之後消息纔算被成功發送。
④設置replication.factor>=3,爲了保證數據安全性,一般會保證每個分區至少有3個副本。
⑤設置min.insync.replicas>1,代表消息至少要被寫入2個副本纔算被髮送成功。min.insync.replicas的默認值爲1,在實際生產中儘量避免默認值爲1。
⑥確保 replication.factor > min.insync.replicas。如果兩者相等,那麼只要有一個副本掛 機,整個分區就無法正常工作了。我們不僅要改善消息的持久性,防止數據丟失,還要 在不降低可用性的基礎上完成。推薦設置成 replication.factor = min.insync.replicas + 1。
⑦設置 unclean.leader.election.enable = false。這是 Broker 端的參數,意思是如果一個 Broker 落後原先的 Leader 太多,就不允許被設置爲新的 Leader。
⑧確保消息消費完成再提交。Consumer 端有個參數 enable.auto.commit,最好把它設 置成 false,並採用手動提交位移的方式。就像前面說的,這對於單 Consumer 多線程 處理的場景而言是至關重要的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章