前言:筆者最近實現了基於可靠訊息方案的分散式事務:Lottor。本文將會介紹Lottor的概況,在後續系列文章介紹具體的實現,歡迎關注。
分散式事務
分散式事務是指事務的參與者、支援事務的伺服器、資源伺服器以及事務管理器分別位於不同的分散式系統的不同節點之上。
首先,解釋下事務的概念:一組操作要麼都完成之後提交,要麼全部回滾。分散式事務特指在分散式環境下,一次事務設計多個服務程式,說白了就是跨程式的事務,這樣就不能控制事務組的一致性。
分散式系統區別於傳統的單體應用,單體應用的服務模組和資料都在一個服務中,使用Spring框架的事務管理器即可滿足事務的屬性。而分散式系統中,來自客戶端的一次請求往往涉及多個服務,事務的一致性問題由此產生。
CAP理論
CAP定理是由加州大學伯克利分校Eric Brewer教授提出來的,他指出WEB服務無法同時滿足一下3個屬性:
- 一致性(Consistency) : 客戶端知道一系列的操作都會同時發生(生效)
- 可用性(Availability) : 每個操作都必須以可預期的響應結束
- 分割槽容錯性(Partition tolerance) : 即使出現單個元件無法可用,操作依然可以完成
具體地講在分散式系統中,在任何資料庫設計中,一個Web應用至多隻能同時支援上面的兩個屬性。
上面這句話的表述,很多人都用過,是的,這是一種誤解。注意CAP定律的完整表述:Any networked shared-data system can have at most two of the three desired properties.
CAP 定律的前提是 P,當 P 決定後才有 CA 的抉擇。因此,簡單粗暴地說「三選二」是有一定誤導性的。
BASE理論
在分散式系統中,我們往往追求的是可用性,它的重要程式比一致性要高,那麼如何實現高可用性呢? 前人已經給我們提出來了另外一個理論,就是BASE理論,它是用來對CAP定理進行進一步擴充的。BASE理論指的是:
- Basically Available(基本可用)
- Soft state(軟狀態)
- Eventually consistent(最終一致性)
BASE理論是對CAP中的一致性和可用性進行一個權衡的結果,理論的核心思想就是:我們無法做到強一致,但每個應用都可以根據自身的業務特點,採用適當的方式來使系統達到最終一致性(Eventual consistency)。
需求分析
功能需求
功能需求最主要的是滿足分散式事務的一致性,涉及的事務組中的操作為多個寫操作,當產生一個或多個寫操作失敗時,回滾整個事務組中的操作。
非功能需求
- 效能:分散式事務對系統的效能必然是有影響的,需要尋找平衡的點。
- 高可用:引入中介軟體或者協調者時,避免單點故障。分散式系統的高可用必然會犧牲部分一致性。
- 可擴充套件:降低引入的業務耦合。
- 伸縮性:系統能夠彈性伸縮。
解決方案
強一致方案
X/Open 組織(即現在的 Open Group )定義了分散式事務處理模型。 X/Open DTP 模型( 1994 )包括應用程式( AP )、事務管理器( TM )、資源管理器( RM )、通訊資源管理器( CRM )四部分。
XA 就是 X/Open DTP 定義的交易中介軟體與資料庫之間的介面規範(即介面函式),交易中介軟體用它來通知資料庫事務的開始、結束以及提交、回滾等。 XA 介面函式由資料庫廠商提供。
2PC
二階段提交的演算法思路可以概括為:參與者將操作成敗通知協調者,再由協調者根據所有參與者的反饋情報決定各參與者是否要提交操作還是中止操作。第一階段:準備階段(投票階段)和第二階段:提交階段(執行階段)。
- 同步阻塞問題。執行過程中,所有參與節點都是事務阻塞型的。當參與者佔有公共資源時,其他第三方節點訪問公共資源不得不處於阻塞狀態。
- 資料不一致。在二階段提交的階段二中,當協調者向參與者傳送commit請求之後,發生了區域性網路異常或者在傳送commit請求過程中協調者發生了故障,這回導致只有一部分參與者接受到了commit請求。而在這部分參與者接到commit請求之後就會執行commit操作。但是其他部分未接到commit請求的機器則無法執行事務提交。於是整個分散式系統便出現了資料部一致性的現象。
- 二階段無法解決的問題:協調者在發出commit訊息之後當機,而唯一接收到這條訊息的參與者同時也當機了。那麼即使協調者通過選舉協議產生了新的協調者,這條事務的狀態也是不確定的,沒人知道事務是否被已經提交。
3PC
三階段提交(Three-phase commit),也叫三階段提交協議(Three-phase commit protocol),是二階段提交(2PC)的改進版本。
如果因為協調者或網路問題,導致參與者遲遲不能收到來自協調者的commit或rollback請求,那麼參與者將不會如兩階段提交中那樣陷入阻塞,而是等待超時後繼續commit。相對於兩階段提交雖然降低了同步阻塞,但仍然無法避免資料的不一致性。在分散式資料庫中,如果期望達到資料的強一致性,那麼服務基本沒有可用性可言,這也是為什麼許多分散式資料庫提供了跨庫事務,但也只是個擺設的原因,在實際應用中我們更多追求的是資料的弱一致性或最終一致性,為了強一致性而丟棄可用性是不可取的。
柔性事務
根據BASE理論,系統並不保證續程式或者執行緒的訪問都會返回最新的更新過的值。系統在資料寫入成功之後,不承諾立即可以讀到最新寫入的值,也不會具體的承諾多久之後可以讀到。
弱一致性的特定形式。系統保證在沒有後續更新的前提下,系統最終返回上一次更新操作的值。在沒有故障發生的前提下,不一致視窗的時間主要受通訊延遲,系統負載和複製副本的個數影響。DNS 是一個典型的最終一致性系統。 在工程實踐上,為了保障系統的可用性,網際網路系統大多將強一致性需求轉換成最終一致性的需求,並通過系統執行冪等性的保證,保證資料的最終一致性。但在電商等場景中,對於資料一致性的解決方法和常見的網際網路系統(如 MySQL 主從同步)又有一定區別。
補償機制:TCC
TCC 其實就是採用的補償機制,其核心思想是:針對每個操作,都要註冊一個與其對應的確認和補償(撤銷)操作。它分為三個階段:
- Try 階段主要是對業務系統做檢測及資源預留
- Confirm 階段主要是對業務系統做確認提交,Try階段執行成功並開始執行Confirm階段時,預設 Confirm階段是不會出錯的。
- Cancel 階段主要是在業務執行錯誤,需要回滾的狀態下執行的業務取消,預留資源釋放。
TCC與2PC協議比較:
- 位於業務服務層而非資源層
- 沒有單獨的準備(Prepare)階段,Try操作兼備資源操作與準備能力
- Try操作可以靈活選擇業務資源的鎖定粒度(以業務定粒度)
- 較高開發成本
本地訊息表
類似於可靠訊息方案。
訊息生產方,需要額外建一個訊息表,並記錄訊息傳送狀態。訊息表和業務資料要在一個事務裡提交,也就是說他們要在一個資料庫裡面。然後訊息會經過MQ傳送到訊息的消費方。如果訊息傳送失敗,會進行重試傳送。
訊息消費方,需要處理這個訊息,並完成自己的業務邏輯。此時如果本地事務處理成功,表明已經處理成功了,如果處理失敗,那麼就會重試執行。如果是業務上面的失敗,可以給生產方傳送一個業務補償訊息,通知生產方進行回滾等操作。
生產方和消費方定時掃描本地訊息表,把還沒處理完成的訊息或者失敗的訊息再傳送一遍。如果有靠譜的自動對賬補賬邏輯,這種方案還是非常實用的。
這種方案遵循BASE理論,採用的是最終一致性,即不會出現像2PC那樣複雜的實現(當呼叫鏈很長的時候,2PC的可用性是非常低的),也不會像TCC那樣可能出現確認或者回滾不了的情況。
優點: 一種非常經典的實現,避免了分散式事務,實現了最終一致性。
缺點: 訊息表會耦合到業務系統中,如果沒有封裝好的解決方案,會有很多雜活需要處理。
事務訊息
RocketMQ第一階段傳送Prepared訊息時,會拿到訊息的地址,第二階段執行本地事物,第三階段通過第一階段拿到的地址去訪問訊息,並修改訊息的狀態。
如果確認訊息傳送失敗了怎麼辦?RocketMQ會定期掃描訊息叢集中的事務訊息,如果發現了Prepared訊息,它會向訊息傳送端(生產者)確認,Bob的錢到底是減了還是沒減呢?如果減了是回滾還是繼續傳送確認訊息呢?RocketMQ會根據傳送端設定的策略來決定是回滾還是繼續傳送確認訊息。這樣就保證了訊息傳送與本地事務同時成功或同時失敗。
如果endTransaction方法執行失敗,資料沒有傳送到broker,導致事務訊息的 狀態更新失敗,broker會有回查執行緒定時(預設1分鐘)掃描每個儲存事務狀態的表格檔案,如果是已經提交或者回滾的訊息直接跳過,如果是prepared狀態則會向Producer發起CheckTransaction請求,Producer會呼叫DefaultMQProducerImpl.checkTransactionState()方法來處理broker的定時回撥請求,而checkTransactionState會呼叫我們的事務設定的決斷方法來決定是回滾事務還是繼續執行,最後呼叫endTransactionOneway讓broker來更新訊息的最終狀態。
-
消費失敗
解決超時問題的思路就是一直重試,直到消費端消費訊息成功 -
消費超時
消費失敗怎麼辦?阿里提供給我們的解決方法是:人工解決。大家可以考慮一下,按照事務的流程,因為某種原因Smith加款失敗,那麼需要回滾整個流程。如果訊息系統要實現這個回滾流程的話,系統複雜度將大大提升,且很容易出現Bug,估計出現Bug的概率會比消費失敗的概率大很多。這也是RocketMQ目前暫時沒有解決這個問題的原因,在設計實現訊息系統時,我們需要衡量是否值得花這麼大的代價來解決這樣一個出現概率非常小的問題,這也是大家在解決疑難問題時需要多多思考的地方。
Lottor介紹
Lottor用於解決微服務架構下分散式事務的問題,基於可靠性訊息事務模型實現。
Lottor的結構
Lottor由三部分組成:
- Lottor Server
- Lottor Client
- Lottor UI
Lottor伺服器與客戶端之間的通訊使用的高效能通訊框架:Netty。所有的客戶端(生產端和消費端)都會與伺服器保持長連線。Lottor UI用於展示系統中的事務組詳細資訊,包括預提交的事務組、消費失敗的事務訊息,並支援頁面操作失敗的訊息(如補償或重試)。
功能介紹
生產方分為三步:
- 預傳送訊息,首先會將消費方的事務組(一條或多條事務訊息)組裝好,併傳送到Lottor Server,事務訊息的狀態為
預傳送
。 - 執行本地事務:預傳送之後,將會執行本地事務。
- 傳送確認訊息:根據本地事務的執行結果,非同步傳送確認訊息。如果本地事務出現異常,回滾本地事務,並將異常資訊捕捉一起傳送到Lottor Server。本地也會持久化該狀態(定期刪除)。
Lottor Server:
- 接收預提交訊息:收到預提交訊息,將事務組中的事務訊息分別儲存,狀態為
pre-commit
。 - 接收確認訊息:狀態為confirm,將更改相應的事務組狀態,並將訊息傳送到對應的消費方(MQ非同步實現),並標記事務訊息的狀態為
unconsumed
。否則,回滾狀態只會修改事務組狀態(定期刪除)。 - 回查預傳送訊息的狀態:狀態為
pre-commit
的事務組訊息,Lottor Server將會定期回查生產方。 - 回查事務訊息的狀態:狀態為
unconsumed
(一般4h),Lottor Server將會定期回查消費方。
消費方:
- 接收事務訊息:訂閱相關的主題,消費完成之後,將會非同步傳送ACK給Lottor Server,消費失敗會將異常返回給Lottor Server。本地也會持久化消費的狀態(定期刪除)。
Lottor 客戶端的持久化,提供了SPI介面,可通過配置動態指定。目前支援:JDBC、Redis、MongoDB和檔案系統。
告警機制及消費補償
這裡所說的告警機制及消費補償是針對消費端,可靠訊息方案是保證了事務訊息一定能夠到達消費方,但是消費方可能因為某些原因而無法成功消費,有些消費異常是可以通過重試解決的,而有些異常是需要告警之後人工干預的。比如消費方暫時不可用,或者是多個消費方消費的順序問題,可以通過定時的重試機制完成。而如果是由於生產方傳送的事務訊息出錯(引數構造錯誤),此時消費方已經提交了本地事務組,所以是無法通過重試實現成功消費,導致需要告警,人為解決髒資料的問題。
適用場景
對於分散式系統的吞吐量有較高的要求,以及能夠滿足最終一致性的場景。如上面提到的告警機制及消費補償,分散式事務是對微服務系統的完善,但是並不能完全保證一致性,可能需要通過告警等手段解決極端問題產生的不一致情況。
專案截圖
總結
本文主要介紹了分散式事務的相關概念以及業界一些常用的解決方案(參考了很多網上的部落格),並提出了筆者基於可靠訊息方案的實現:Lottor。後續文章將會詳細介紹Lottor的實現,敬請期待。