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