訊息佇列高手課 筆記5
我們日常使用事務的場景,絕大部分都是在操作資料庫的時候。像 MySQL、Oracle 這些主流的關係型資料庫,也都提供了完整的事務實現。那訊息佇列為什麼也需要事務呢?
其實很多場景下,我們“發訊息”這個過程,目的往往是通知另外一個系統或者模組去更新資料,訊息佇列中的“事務”,主要解決的是訊息生產者和訊息消費者的資料一致性問題。
依然拿我們熟悉的電商來舉個例子。一般來說,使用者在電商 APP 上購物時,先把商品加到購物車裡,然後幾件商品一起下單,最後支付,完成購物流程,就可以愉快地等待收貨了。
這個過程中有一個需要用到訊息佇列的步驟,訂單系統建立訂單後,發訊息給購物車系統,將已下單的商品從購物車中刪除。
因為從購物車刪除已下單商品這個步驟,並不是使用者下單支付這個主要流程中必需的步驟,使用訊息佇列來非同步清理購物車是更加合理的設計。
對於訂單系統來說,它建立訂單的過程中實際上執行了 2 個步驟的操作:
1.在訂單庫中插入一條訂單資料,建立訂單;
2.發訊息給訊息佇列,訊息的內容就是剛剛建立的訂單。
購物車系統訂閱相應的主題,接收訂單建立的訊息,然後清理購物車,在購物車中刪除訂單中的商品。
在分散式系統中,上面提到的這些步驟,任何一個步驟都有可能失敗,如果不做任何處理,那就有可能出現訂單資料與購物車資料不一致的情況,比如說:
1.建立了訂單,沒有清理購物車;
2.訂單沒建立成功,購物車裡面的商品卻被清掉了。
那我們需要解決的問題可以總結為:在上述任意步驟都有可能失敗的情況下,還要保證訂單庫和購物車庫這兩個庫的資料一致性。
對於購物車系統收到訂單建立成功訊息清理購物車這個操作來說,失敗的處理比較簡單,
只要成功執行購物車清理後再提交消費確認即可,如果失敗,由於沒有提交消費確認,訊息佇列會自動重試。
問題的關鍵點集中在訂單系統,建立訂單和傳送訊息這兩個步驟要麼都操作成功,要麼都操作失敗,不允許一個成功而另一個失敗的情況出現。
這就是事務需要解決的問題。
什麼是分散式事務?
那什麼是事務呢?如果我們需要對若干資料進行更新操作,為了保證這些資料的完整性和一致性,
我們希望這些更新操作要麼都成功,要麼都失敗。至於更新的資料,不只侷限於資料庫中的資料,
可以是磁碟上的一個檔案,也可以是遠端的一個服務,或者以其他形式儲存的資料。
這就是通常我們理解的事務。其實這段對事務的描述不是太準確也不完整,但是,它更易於理解,大體上也是正確的。所以我還是傾向於這樣來講“事務”這個比較抽象的概念。
一個嚴格意義的事務實現,應該具有 4 個屬性:原子性、一致性、隔離性、永續性。這四個屬性通常稱為 ACID 特性。
**原子性**,是指一個事務操作不可分割,要麼成功,要麼失敗,不能有一半成功一半失敗的情況。
**一致性**,是指這些資料在事務執行完成這個時間點之前,讀到的一定是更新前的資料,之後讀到的一定是更新後的資料,不應該存在一個時刻,讓使用者讀到更新過程中的資料。
**隔離性**,是指一個事務的執行不能被其他事務干擾。即一個事務內部的操作及使用的資料對正在進行的其他事務是隔離的,併發執行的各個事務之間不能互相干擾,這個有點兒像我們打網遊中的副本,我們在副本中打的怪和掉的裝備,與其他副本沒有任何關聯也不會互相影響。
**永續性**,是指一個事務一旦完成提交,後續的其他操作和故障都不會對事務的結果產生任何影響。
大部分傳統的單體關係型資料庫都完整的實現了 ACID,但是,對於分散式系統來說,嚴格的實現 ACID 這四個特性幾乎是不可能的,或者說實現的代價太大,大到我們無法接受。
分散式事務就是要在分散式系統中的實現事務。在分散式系統中,在保證可用性和不嚴重犧牲效能的前提下,光是要實現資料的一致性就已經非常困難了,所以出現了很多“殘血版”的一致性,比如順序一致性、最終一致性等等。
顯然實現嚴格的分散式事務是更加不可能完成的任務。所以,目前大家所說的分散式事務,更多情況下,是在分散式系統中事務的不完整實現。在不同的應用場景中,有不同的實現,目的都是通過一些妥協來解決實際問題。
在實際應用中,比較常見的分散式事務實現有 2PC(Two-phase Commit,也叫二階段提交)、TCC(Try-Confirm-Cancel) 和事務訊息。每一種實現都有其特定的使用場景,也有各自的問題,都不是完美的解決方案。
事務訊息適用的場景主要是那些需要非同步更新資料,並且對資料實時性要求不太高的場景。比如我們在開始時提到的那個例子,在建立訂單後,如果出現短暫的幾秒,購物車裡的商品沒有被及時清空,也不是完全不可接受的,只要最終購物車的資料和訂單資料保持一致就可以了。
2PC 和 TCC 不是我們本次課程討論的內容,就不展開講了,感興趣的同學可以自行學習。
訊息佇列是如何實現分散式事務的?
事務訊息需要訊息佇列提供相應的功能才能實現,Kafka 和 RocketMQ 都提供了事務相關功能。
回到訂單和購物車這個例子,我們一起來看下如何用訊息佇列來實現分散式事務。
首先,訂單系統在訊息佇列上開啟一個事務。然後訂單系統給訊息伺服器傳送一個“半訊息”,這個半訊息不是說訊息內容不完整,
它包含的內容就是完整的訊息內容,半訊息和普通訊息的唯一區別是,在事務提交之前,對於消費者來說,這個訊息是不可見的。
半訊息傳送成功後,訂單系統就可以執行本地事務了,在訂單庫中建立一條訂單記錄,並提交訂單庫的資料庫事務。然後根據本地事務的執行結果決定提交或者回滾事務訊息。
如果訂單建立成功,那就提交事務訊息,購物車系統就可以消費到這條訊息繼續後續的流程。如果訂單建立失敗,那就回滾事務訊息,購物車系統就不會收到這條訊息。
這樣就基本實現了“要麼都成功,要麼都失敗”的一致性要求。
如果你足夠細心,可能已經發現了,這個實現過程中,有一個問題是沒有解決的。如果在第四步提交事務訊息時失敗了怎麼辦?
對於這個問題,Kafka 和 RocketMQ 給出了 2 種不同的解決方案。
Kafka 的解決方案比較簡單粗暴,直接丟擲異常,讓使用者自行處理。我們可以在業務程式碼中反覆重試提交,直到提交成功,或者刪除之前建立的訂單進行補償。
RocketMQ 則給出了另外一種解決方案。
RocketMQ 中的分散式事務實現
在 RocketMQ 中的事務實現中,增加了事務反查的機制來解決事務訊息提交失敗的問題。如果 Producer 也就是訂單系統,
在提交或者回滾事務訊息時發生網路異常,RocketMQ 的 Broker 沒有收到提交或者回滾的請求,Broker 會定期去 Producer 上反查這個事務對應的本地事務的狀態,
然後根據反查結果決定提交或者回滾這個事務。
為了支撐這個事務反查機制,我們的業務程式碼需要實現一個反查本地事務狀態的介面,告知 RocketMQ 本地事務是成功還是失敗。
在我們這個例子中,反查本地事務的邏輯也很簡單,我們只要根據訊息中的訂單 ID,在訂單庫中查詢這個訂單是否存在即可,如果訂單存在則返回成功,否則返回失敗。RocketMQ 會自動根據事務反查的結果提交或者回滾事務訊息。
這個反查本地事務的實現,並不依賴訊息的傳送方,也就是訂單服務的某個例項節點上的任何資料。這種情況下,即使是傳送事務訊息的那個訂單服務節點宕機了,RocketMQ 依然可以通過其他訂單服務的節點來執行反查,確保事務的完整性。
綜合上面講的通用事務訊息的實現和 RocketMQ 的事務反查機制,使用 RocketMQ 事務訊息功能實現分散式事務的流程如下圖:
小結
我們通過一個訂單購物車的例子,學習了事務的 ACID 四個特性,以及如何使用訊息佇列來實現分散式事務。然後我們給出了現有的幾種分散式事務的解決方案,包括事務訊息,但是這幾種方案都不能解決分散式系統中的所有問題,每一種方案都有侷限性和特定的適用場景。最後,我們一起學習了 RocketMQ 的事務反查機制,這種機制通過定期反查事務狀態,來補償提交事務訊息可能出現的通訊失敗。在 Kafka 的事務功能中,並沒有類似的反查機制,需要使用者自行去解決這個問題。但是,這不代表 RocketMQ 的事務功能比 Kafka 更好,只能說在我們這個例子的場景下,更適合使用 RocketMQ。Kafka 對於事務的定義、實現和適用場景,和 RocketMQ 有比較大的差異,後面的課程中,我們會專門講到 Kafka 的事務的實現原理。