RabbitMQ,RocketMQ,Kafka 事務性,訊息丟失和訊息重複傳送的處理策略

Rick.lz發表於2021-12-30

訊息佇列常見問題處理

分散式事務

什麼是分散式事務

我們的伺服器從單機發展到擁有多臺機器的分散式系統,各個系統之前需要藉助於網路進行通訊,原有單機中相對可靠的方法呼叫以及程式間通訊方式已經沒有辦法使用,同時網路環境也是不穩定的,造成了我們多個機器之間的資料同步問題,這就是典型的分散式事務問題。

在分散式事務中事務的參與者、支援事務的伺服器、資源伺服器以及事務管理器分別位於不同的分散式系統的不同節點之上。分散式事務就是要保證不同節點之間的資料一致性。

常見的分散式事務解決方案

1、2PC(二階段提交)方案 - 強一致性

2、3PC(三階段提交)方案

3、TCC (Try-Confirm-Cancel)事務 - 最終一致性

4、Saga事務 - 最終一致性

5、本地訊息表 - 最終一致性

6、MQ事務 - 最終一致性

這裡重點關注下使用訊息佇列實現分散式的一致性,上面幾種的分散式設計方案的具體細節可參見文章最後的引用連結

基於 MQ 實現的分散式事務

本地訊息表-最終一致性

訊息的生產方,除了維護自己的業務邏輯之外,同時需要維護一個訊息表。這個訊息表裡面記錄的就是需要同步到別的服務的資訊,當然這個訊息表,每個訊息都有一個狀態值,來標識這個訊息有沒有被成功處理。

傳送放的業務邏輯以及訊息表中資料的插入將在一個事務中完成,這樣避免了業務處理成功 + 事務訊息傳送失敗,或業務處理失敗 + 事務訊息傳送成功,這個問題。

mq

舉個例子:

我們假定目前有兩個服務,訂單服務,購物車服務,使用者在購物車中對幾個商品進行合併下單,之後需要清空購物車中剛剛已經下單的商品資訊。

1、訊息的生產方也就是訂單服務,完成了自己的邏輯(對商品進行下單操作)然後把這個訊息通過 mq 傳送到需要進行資料同步的其他服務中,也就是我們栗子中的購物車服務。

2、其他服務(購物車服務)會監聽這個佇列;

1、如果收到這個訊息,並且資料同步執行成功了,當然這也是一個本地事務,就通過 mq 回覆訊息的生產方(訂單服務)訊息已經處理了,然後生產方就能標識本次事務已經結束。如果是一個業務上的錯誤,就回復訊息的生產方,需要進行資料回滾了。

2、很久沒收到這個訊息,這種情況是不會發生的,訊息的傳送方會有一個定時的任務,會定時重試傳送訊息表中還沒有處理的訊息;

3、訊息的生產方(訂單服務)如果收到訊息回執;

1、成功的話就修改本次訊息已經處理完,也就是本次分散式事務的同步已經完成;

2、如果訊息的結果是執行失敗,同時在本地回滾本次事務,標識訊息已經處理完成;

3、如果訊息丟失,也就是回執訊息沒有收到,這種情況也不太會發生,訊息的傳送方(訂單服務)會有一個定時的任務,定時重試傳送訊息表中還沒有處理的訊息,下游的服務需要做冪等,可能會收到多次重複的訊息,如果一個回覆訊息生產方中的某個回執資訊丟失了,後面持續收到生產方的 mq 訊息,然後再次回覆訊息的生產方回執資訊,這樣總能保證傳送者能成功收到回執,訊息的生產方在接收回執訊息的時候也要做到冪等性。

這裡有兩個很重要的操作:

1、伺服器處理訊息需要是冪等的,訊息的生產方和接收方都需要做到冪等性;

2、傳送放需要新增一個定時器來遍歷重推未處理的訊息,避免訊息丟失,造成的事務執行斷裂。

該方案的優缺點

優點:

1、在設計層面上實現了訊息資料的可靠性,不依賴訊息中介軟體,弱化了對 mq 特性的依賴。

2、簡單,易於實現。

缺點:

主要是需要和業務資料繫結到一起,耦合性比較高,使用相同的資料庫,會佔用業務資料庫的一些資源。

MQ事務-最終一致性

下面分析下幾種訊息佇列對事務的支援

RocketMQ中如何處理事務

RocketMQ 中的事務,它解決的問題是,確保執行本地事務和發訊息這兩個操作,要麼都成功,要麼都失敗。並且,RocketMQ 增加了一個事務反查的機制,來儘量提高事務執行的成功率和資料一致性。

mq

主要是兩個方面,正常的事務提交和事務訊息補償

正常的事務提交

1、傳送訊息(half訊息),這個 half 訊息和普通訊息的區別,在事務提交 之前,對於消費者來說,這個訊息是不可見的。

2、MQ SERVER寫入資訊,並且返回響應的結果;

3、根據MQ SERVER響應的結果,決定是否執行本地事務,如果MQ SERVER寫入資訊成功執行本地事務,否則不執行;

4、根據本地事務執行的狀態,決定是否對事務進行 Commit 或者 Rollback。MQ SERVER收到 Commit,之後就會投遞該訊息到下游的訂閱服務,下游的訂閱服務就能進行資料同步,如果是 Rollback 則該訊息就會被丟失;

如果MQ SERVER沒有收到 Commit 或者 Rollback 的訊息,這種情況就需要進行補償流程了

補償流程

1、MQ SERVER如果沒有收到來自訊息傳送方的 Commit 或者 Rollback 訊息,就會向訊息傳送端也就是我們的伺服器發起一次查詢,查詢當前訊息的狀態;

2、訊息傳送方收到對應的查詢請求,查詢事務的狀態,然後把狀態重新推送給MQ SERVERMQ SERVER就能之後後續的流程了。

相比於本地訊息表來處理分散式事務,MQ 事務是把原本應該在本地訊息表中處理的邏輯放到了 MQ 中來完成。

Kafka中如何處理事務

Kafka 中的事務解決問題,確保在一個事務中傳送的多條資訊,要麼都成功,要麼都失敗。也就是保證對多個分割槽寫入操作的原子性。

通過配合 Kafka 的冪等機制來實現 Kafka 的 Exactly Once,滿足了讀取-處理-寫入這種模式的應用程式。當然 Kafka 中的事務主要也是來處理這種模式的。

什麼是讀取-處理-寫入模式呢?

慄如:在流計算中,用 Kafka 作為資料來源,並且將計算結果儲存到 Kafka 這種場景下,資料從 Kafka 的某個主題中消費,在計算叢集中計算,再把計算結果儲存在 Kafka 的其他主題中。這個過程中,要保證每條訊息只被處理一次,這樣才能保證最終結果的成功。Kafka 事務的原子性就保證了,讀取和寫入的原子性,兩者要不一起成功,要不就一起失敗回滾。

這裡來分析下 Kafka 的事務是如何實現的

它的實現原理和 RocketMQ 的事務是差不多的,都是基於兩階段提交來實現的,在實現上可能更麻煩

先來介紹下事務協調者,為了解決分散式事務問題,Kafka 引入了事務協調者這個角色,負責在服務端協調整個事務。這個協調者並不是一個獨立的程式,而是 Broker 程式的一部分,協調者和分割槽一樣通過選舉來保證自身的可用性。

Kafka 叢集中也有一個特殊的用於記錄事務日誌的主題,裡面記錄的都是事務的日誌。同時會有多個協調者的存在,每個協調者負責管理和使用事務日誌中的幾個分割槽。這樣能夠並行的執行事務,提高效能。

下面看下具體的流程

  • 1、首先在開啟事務的時候,生產者會給協調者傳送一個開啟事務的請求,協調者在事務日誌中記錄下事務ID;

  • 2、然後生產者開始傳送事務訊息給協調者,不過需要先傳送訊息告知協調者在哪個主題和分割槽,之後就正常的傳送事務訊息,這些事務訊息不像 RocketMQ 會儲存在特殊的佇列中,Kafka 未提交的事務訊息和普通的訊息一樣,只是在消費的時候依賴客戶端進行過濾。

  • 3、訊息傳送完成,生產者根據自己的執行的狀態對協調者進行事務的提交或者回滾;

事務的提交

1、協調者設定事務的狀態為PrepareCommit,寫入到事務日誌中;

2、協調者在每個分割槽中寫入事務結束的標識,然後客戶端就能把之前過濾的未提交的事務訊息放行給消費端進行消費了;

事務的回滾

1、協調者設定事務的狀態為PrepareAbort,寫入到事務日誌中;

2、協調者在每個分割槽中寫入事務回滾的標識,然後之前未提交的事務訊息就能被丟棄了;

這裡引用一下【訊息佇列高手課中的圖片】

mq

RabbitMQ中的事務

RabbitMQ 中事務解決的問題是確保生產者的訊息到達MQ SERVER,這和其他 MQ 事務還是有點差別的,這裡也不展開討論了。

訊息防丟失

先來分析下一條訊息在 MQ 中流轉所經歷的階段。

mq

生產階段:生產者產生訊息,通過網路傳送到 Broker 端。

儲存階段:Broker 拿到訊息,需要進行落盤,如果是叢集版的 MQ 還需要同步資料到其他節點。

消費階段:消費者在 Broker 端拉資料,通過網路傳輸到達消費者端。

生產階段防止訊息丟失

發生網路丟包、網路故障等這些會導致訊息的丟失

RabbitMQ 中的防丟失措施

  • 1、對於可以感知的錯誤,我們捕獲錯誤,然後重新投遞;

  • 2、通過 RabbitMQ 中的事務解決,RabbitMQ 中的事務解決的就是生產階段訊息丟失的問題;

在生產者傳送訊息之前,通過channel.txSelect開啟一個事務,接著傳送訊息, 如果訊息投遞 server 失敗,進行事務回滾channel.txRollback,然後重新傳送, 如果 server 收到訊息,就提交事務channel.txCommit

不過使用事務效能不好,這是同步操作,一條訊息傳送之後會使傳送端阻塞,以等待RabbitMQ Server的回應,之後才能繼續傳送下一條訊息,生產者生產訊息的吞吐量和效能都會大大降低。

  • 3、使用傳送確認機制。

使用確認機制,生產者將通道設定成 confirm 確認模式,一旦通道進入 confirm 模式,所有在該通道上面釋出的訊息都會被指派一個唯一的ID(從1開始),一旦訊息被投遞到所有匹配的佇列之後,RabbitMQ 就會傳送一個確認(Basic.Ack)給生產者(包含訊息的唯一 deliveryTag 和 multiple 引數),這就使得生產者知曉訊息已經正確到達了目的地了。

multiple 為 true 表示的是批量的訊息確認,為 true 的時候,表示小於等於返回的 deliveryTag 的訊息 id 都已經確認了,為 false 表示的是訊息 id 為返回的 deliveryTag 的訊息,已經確認了。

mq

確認機制有三種型別

1、同步確認

2、批量確認

3、非同步確認

同步模式的效率很低,因為每一條訊息度都需要等待確認好之後,才能處理下一條;

批量確認模式相比同步模式效率是很高,不過有個致命的缺陷,一旦回覆確認失敗,當前確認批次的訊息會全部重新傳送,導致訊息重複傳送;

非同步模式就是個很好的選擇了,不會有同步模式的阻塞問題,同時效率也很高,是個不錯的選擇。

Kafka 中的防丟失措施

Kafaka 中引入了一個 broker。 broker 會對生產者和消費者進行訊息的確認,生產者傳送訊息到 broker,如果沒有收到 broker 的確認就可以選擇繼續傳送。

只要 Producer 收到了 Broker 的確認響應,就可以保證訊息在生產階段不會丟失。有些訊息佇列在長時間沒收到傳送確認響應後,會自動重試,如果重試再失敗,就會以返回值或者異常的方式告知使用者。

只要正確處理 Broker 的確認響應,就可以避免訊息的丟失。

RocketMQ 中的防丟失措施

  • 使用 SYNC 的傳送訊息方式,等待 broker 處理結果

RocketMQ 提供了3種傳送訊息方式,分別是:

同步傳送:Producer 向 broker 傳送訊息,阻塞當前執行緒等待 broker 響應 傳送結果。

非同步傳送:Producer 首先構建一個向 broker 傳送訊息的任務,把該任務提交給執行緒池,等執行完該任務時,回撥使用者自定義的回撥函式,執行處理結果。

Oneway傳送:Oneway 方式只負責傳送請求,不等待應答,Producer 只負責把請求發出去,而不處理響應結果。

  • 使用事務,RocketMQ 中的事務,它解決的問題是,確保執行本地事務和發訊息這兩個操作,要麼都成功,要麼都失敗。

儲存階段

在儲存階段正常情況下,只要 Broker 在正常執行,就不會出現丟失訊息的問題,但是如果 Broker 出現了故障,比如程式死掉了或者伺服器當機了,還是可能會丟失訊息的。

RabbitMQ 中的防丟失措施

防止在儲存階段訊息額丟失,可以做持久化,防止異常情況(重啟,關閉,當機)。。。

RabbitMQ 持久化中有三部分:

  • 交換器的持久化

交換器的持久化,是通過在宣告佇列時將 durable 引數置為 true 實現的,不設定持久化的話,交換器的資訊將會丟失。

  • 佇列持久化

佇列的持久化,是通過在宣告佇列時將 durable 引數置為 true 實現的,佇列的持久化能保證其本身的後設資料不會因異常情況而丟失,但是並不能保證內部所儲存的訊息不會丟失。

  • 訊息的持久化

訊息的持久化,在投遞時指定 delivery_mode=2(1是非持久化),訊息的持久化,需要配合佇列的持久,只設定訊息的持久化,重啟之後佇列消失,繼而訊息也會丟失。所以如果只設定訊息持久化而不設定佇列的持久化意義不大。

對於持久化,如果所有的訊息都設定持久化,會影響寫入的效能,所以可以選擇對可靠性要求比較高的訊息進行持久化處理。

不過訊息持久化並不能百分之百避免訊息的丟失

比如資料在落盤的過程中當機了,訊息還沒及時同步到記憶體中,這也是會丟資料的,這種問題可以通過引入映象佇列來解決。

映象佇列的作用:引入映象佇列,可已將佇列映象到叢集中的其他 Broker 節點之上,如果叢集中的一個節點失效了,佇列能夠自動切換到映象中的另一個節點上來保證服務的可用性。(更細節的這裡不展開討論了)

Kafka 中的防丟失措施

作業系統本身有一層快取,叫做 Page Cache,當往磁碟檔案寫入的時候,系統會先將資料流寫入快取中。

Kafka 收到訊息後也會先儲存在也快取中(Page Cache)中,之後由作業系統根據自己的策略進行刷盤或者通過 fsync 命令強制刷盤。如果系統掛掉,在 PageCache 中的資料就會丟失。也就是對應的 Broker 中的資料就會丟失了。

mq

處理思路

1、控制競選分割槽 leader 的 Broker。如果一個 Broker 落後原先的 Leader 太多,那麼它一旦成為新的 Leader,必然會造成訊息的丟失。

2、控制訊息能夠被寫入到多個副本中才能提交,這樣避免上面的問題1。

RocketMQ 中的防丟失措施

1、將刷盤方式改成同步刷盤;

2、對於多個節點的 Broker,需要將 Broker 叢集配置成:至少將訊息傳送到 2 個以上的節點,再給客戶端回覆傳送確認響應。這樣當某個 Broker 當機時,其他的 Broker 可以替代當機的 Broker,也不會發生訊息丟失。

消費階段

消費階段就很簡單了,如果在網路傳輸中丟失,這個訊息之後還會持續的推送給消費者,在消費階段我們只需要控制在業務邏輯處理完成之後再去進行消費確認就行了。

總結:對於訊息的丟失,也可以藉助於本地訊息表的思路,訊息產生的時候進行訊息的落盤,長時間未處理的訊息,使用定時重推到佇列中。

訊息重複傳送

訊息在 MQ 中的傳遞,大致可以歸類為下面三種:

1、At most once: 至多一次。訊息在傳遞時,最多會被送達一次。是不安全的,可能會丟資料。

2、At least once: 至少一次。訊息在傳遞時,至少會被送達一次。也就是說,不允許丟訊息,但是允許有少量重複訊息出現。

3、Exactly once:恰好一次。訊息在傳遞時,只會被送達一次,不允許丟失也不允許重複,這個是最高的等級。

大部分訊息佇列滿足的都是At least once,也就是可以允許重複的訊息出現。

我們消費者需要滿足冪等性,通常有下面幾種處理方案

1、利用資料庫的唯一性

根據業務情況,選定業務中能夠判定唯一的值作為資料庫的唯一鍵,新建一個流水錶,然後執行業務操作和流水錶資料的插入放在同一事務中,如果流水錶資料已經存在,那麼就執行失敗,藉此保證冪等性。也可先查詢流水錶的資料,沒有資料然後執行業務,插入流水錶資料。不過需要注意,資料庫讀寫延遲的情況。

2、資料庫的更新增加前置條件

3、給訊息帶上唯一ID

每條訊息加上唯一ID,利用方法1中通過增加流水錶,藉助資料庫的唯一性來處理重複訊息的消費。

參考

【訊息佇列高手課】https://time.geekbang.org/column/intro/100032301
【訊息佇列設計精要】https://tech.meituan.com/2016/07/01/mq-design.html
【RabbitMQ實戰指南】https://book.douban.com/subject/27591386/
【分散式事務最經典的七種解決方案】https://segmentfault.com/a/1190000040321750
【分散式事務的實現原理】https://draveness.me/distributed-transaction-principle/
【理解分散式事務】https://juejin.cn/post/6844903734753886216
【設計(design)】https://github.com/apache/rocketmq/blob/master/docs/cn/design.md
【Kafka 是如何實現事務的?】https://zhuanlan.zhihu.com/p/163683403
【MQ - RabbitMQ Cluster】https://www.cnblogs.com/Neeo/articles/13915836.html
【如何避免訊息丟失?】https://www.lixueduan.com/post/kafka/09-avoid-msg-lost/
【RabbitMQ,RocketMQ,Kafka 事務性,訊息丟失和訊息重複傳送的處理策略】https://boilingfrog.github.io/2021/12/30/訊息佇列中事務性訊息丟失重複傳送的處理/#rocketmq-中的防丟失措施

相關文章