分散式事務的這些常見用法都有坑,來看看正確姿勢

葉東富發表於2021-11-29

隨著微服務架構的流行,隨之而來就必然遇到跨服務的分散式事務這個難題。分散式事務之所以難,主要是因為分散式系統中的各個節點都可能發生各種非預期的情況。本文先介紹分散式系統中的異常問題,然後介紹這些問題帶給分散式事務的挑戰,接下來指出現有各種常見用法的問題,最後給出正確的方案。

NPC的挑戰

分散式系統最大的敵人可能就是NPC了,在這裡它是Network Delay, Process Pause, Clock Drift的首字母縮寫。我們先看看具體的NPC問題是什麼:

  • Network Delay,網路延遲。雖然網路在多數情況下工作的還可以,雖然TCP保證傳輸順序和不會丟失,但它無法消除網路延遲問題。
  • Process Pause,程式暫停。有很多種原因可以導致程式暫停:比如程式語言中的GC(垃圾回收機制)會暫停所有正在執行的執行緒;再比如,我們有時會暫停雲伺服器,從而可以在不重啟的情況下將雲伺服器從一臺主機遷移到另一臺主機。我們無法確定性預測程式暫停的時長,你以為持續幾百毫秒已經很長了,但實際上持續數分鐘之久程式暫停並不罕見。
  • Clock Drift,時鐘漂移。現實生活中我們通常認為時間是平穩流逝,單調遞增的,但在計算機中不是。計算機使用時鐘硬體計時,通常是石英鐘,計時精度有限,同時受機器溫度影響。為了在一定程度上同步網路上多個機器之間的時間,通常使用NTP協議將本地裝置的時間與專門的時間伺服器對齊,這樣做的一個直接結果是裝置的本地時間可能會突然向前或向後跳躍。

分散式事務既然是分散式的系統,自然也有NPC問題。因為沒有涉及時間戳,帶來的困擾主要是NP。

TCC的空補償與懸掛

我們以分散式事務中的TCC(如果是對TCC還不瞭解的同學,可以參考這篇文章,分散式事務最經典的七種解決方案,瞭解分散式事務相關的基礎知識)作為例子,看看NP帶來的影響。

一般情況下,一個TCC回滾時的執行順序是,先執行完Try,再執行Cancel,但是由於N,則有可能Try的網路延遲大,導致先執行Cancel,再執行Try。

這種情況就引入了分散式事務中的兩個難題:

  1. 空補償:Cancel執行時,Try未執行,事務分支的Cancel操作需要判斷出Try未執行,這時需要忽略Cancel中的業務資料更新,直接返回
  2. 懸掛:Try執行時,Cancel已執行完成,事務分支的Try操作需要判斷出Cancel一致性,這時需要忽略Try中的業務資料更新,直接返回

分散式事務還有一類需要處理的常見問題,就是重複請求,業務需要做冪等處理。因為空補償、懸掛、重複請求都跟NP有關,我們把他們統稱為子事務亂序問題。在業務處理中,需要小心處理好這三種問題,否則會出現錯誤資料。

現有方案的問題

我們看到開源專案https://github.com/yedf/dtm之外,包括各雲廠商,各開源專案,他們給出的業務實現建議大多類似如下:

  • 空補償:“針對該問題,在服務設計時,需要允許空補償,即在沒有找到要補償的業務主鍵時,返回補償成功,並將原業務主鍵記錄下來,標記該業務流水已補償成功。”
  • 防懸掛:“需要檢查當前業務主鍵是否已經在空補償記錄下來的業務主鍵中存在,如果存在則要拒絕執行該筆服務,以免造成資料不一致。”

上述的這種實現,能夠在大部分情況下正常執行,但是上述做法中的“先查後改”在併發情況下是容易掉坑裡的,我們分析一下如下場景:

  • 正常執行順序下,Try執行時,在查完沒有空補償記錄的業務主鍵之後,事務提交之前,如果發生了程式暫停P,或者事務內部進行網路請求出現了擁塞,導致本地事務等待較久
  • 全域性事務超時後,Cancel執行,因為沒有查到要補償的業務主鍵,因此判斷是空補償,直接返回
  • Try的程式暫停結束,最後提交本地事務
  • 全域性事務回滾完成後,Try分支的業務操作沒有被回滾,產生了懸掛

事實上,NPC裡的P和C,以及P和C的組合,有很多種的場景,都可以導致上述競態情況,就不一一贅述了。

雖然這種情況發生的機率不高,但是在金融領域,一旦涉及金錢賬目,那麼帶來的影響可能是巨大的。

PS:冪等控制如果也採用“先查再改”,也是一樣很容易出現類似的問題。解決這一類問題的關鍵點是要利用唯一索引,“以改代查”來避免競態條件。

正確姿勢

下面我們來詳解yedf/dtm是如何解決這個問題的。

dtm首創了子事務屏障技術,用於同時解決空補償、防懸掛、冪等這三個問題,對於TCC事務,他的詳細工作過程如下:

  1. 在本地資料庫中建立好子事務屏障表dtm_barrier.barrier,唯一索引為gid-branchid-branchop
  2. 對於Try、Confirm、Cancel操作,insert ignore一條記錄gid-branchid-try|confirm|cancel,如果影響行數為0(重複請求、懸掛),直接提交返回
  3. 對於Cancel操作額外再insert ingore一條記錄 gid-branchid-try,如果影響行數為1(空補償),直接提交返回
  4. 執行業務邏輯並提交返回,如果業務發生錯誤則回滾

假如Try和Cancel的執行時間沒有重疊,那麼讀者容易分析出上述過程能夠解決空補償和懸掛問題。如果出現了Try和Cancel執行時間重疊的情況,我們看看會發生什麼。

假設Try和Cancel併發執行,Cancel和Try都會插入同一條記錄gid-branchid-try,由於唯一索引衝突,那麼兩個操作中只有一個能夠成功,而另一個則會等持有鎖的事務完成後返回。

  • 情況1,Try插入gid-branchid-try失敗,Cancel操作插入gid-branchid-try成功,此時就是典型的空補償和懸掛場景,按照子事務屏障演算法,Try和Cancel都會直接返回
  • 情況2,Try插入gid-branchid-try成功,Cancel操作插入gid-branchid-try失敗,按照上述子事務屏障演算法,會正常執行業務,而且業務執行的順序是Try在Cancel前
  • 情況3,Try和Cancel的操作在重疊期間又遇見當機等情況,那麼至少Cancel會被dtm重試,那麼最終會走到情況1或2。

綜上各種情況的詳細論述,子事務屏障能夠在各種NP情況下,保證最終結果的正確性。

事實上,子事務屏障有大量優點,包括:

  • 兩個insert判斷解決空補償、防懸掛、冪等這三個問題,比其他方案的三種情況分別判斷,邏輯複雜度大幅降低
  • dtm的子事務屏障是SDK層解決這三個問題,業務完全不需要關心
  • 效能高,對於正常完成的事務(一般失敗的事務不超過1%),子事務屏障的額外開銷是每個分支操作一個SQL,比其他方案代價更小。

上述的理論與分析過程也同樣適用於SAGA分散式事務。dtm裡面的子事務屏障同時支援了TCC和SAGA兩種事務模式。

完整的解決方案

DTM是一款golang開發的分散式事務管理器,解決了跨資料庫、跨服務、跨語言棧更新資料的一致性問題。

下面是dtm和阿里開源的seata的主要特性對比:

特性DTMSEATA備註
支援語言Go、Java、python、php、c#...Javadtm可輕鬆接入一門新語言
異常處理子事務屏障自動處理手動處理dtm解決了冪等、懸掛、空補償
TCC事務
XA事務
AT事務建議使用XAAT與XA類似,效能更好,但有髒回滾
SAGA事務支援併發狀態機模式
事務訊息dtm提供類似rocketmq的事務訊息
單服務多資料來源
通訊協議HTTP、gRPCdubbo等協議dtm對雲原生更加友好

如果您的語言棧包含了Java之外的語言,那麼dtm是您的首選。如果您的語言棧是Java,您也可以選擇接入dtm,使用子事務屏障技術,簡化您的業務編寫,可以參考用Java輕鬆完成一個TCC分散式事務,自動處理空補償、懸掛、冪等

如果您想要學習分散式事務相關的知識,dtm的文件備受好評,能夠讓讀者快速入門分散式事務,理論結合實踐,讓讀者逐步深入。

歡迎大家訪問https://github.com/yedf/dtm,歡迎Issue、PR、Star

相關文章