分佈式事務 Seata AT模式原理與實戰

Seata 是阿里開源的基於Java的分佈式事務解決方案

AT,XA,TCC,Saga

Seata 提供四種模式解決分佈式事務場景,AT,XA,TCC,Saga。簡單叨咕叨咕我對這幾種模式的理解html

AT

image.png

這是Seata的一大特點,AT對業務代碼徹底無侵入性,使用很是簡單,改形成本低。咱們只須要關注本身的業務SQL,Seata會經過分析咱們業務SQL,反向生成回滾數據java

AT 包含兩個階段git

  • 一階段,全部參與事務的分支,本地事務Commit 業務數據和回滾日誌(undoLog)
  • 二階段,事務協調者根據全部分支的狀況,決定本次全局事務是Commit 仍是 Rollback(二階段是徹底異步)
XA

也是咱們常說的二階段提交,XA要求數據庫自己提供對規範和協議的支持。XA用起來的話,也是對業務代碼無侵入性的。github

上述其餘三種模式,都是屬於補償型,沒法保證全局一致性。啥意思呢,例如剛剛說的AT模式,咱們是可能讀到這一次分佈式事務的中間狀態,而XA模式不會。spring

補償型 事務處理機制構建在 事務資源(數據庫) 之上(要麼在中間件層面,要麼在應用層面),事務資源 自己對分佈式事務是無感知的,這也就致使了補償型事務沒法作到真正的 全局一致性 。
好比,一條庫存記錄,處在 補償型 事務處理過程當中,由 100 扣減爲 50。此時,倉庫管理員鏈接數據庫,查詢統計庫存,就看到當前的 50。以後,事務由於意外回滾,庫存會被補償回滾爲 100。顯然,倉庫管理員查詢統計到的 50 就是 髒 數據。
若是是XA的話,中間態數據庫存 50 由數據庫自己保證,不會被倉庫管理員讀到(固然隔離級別須要 讀已提交 以上)

可是全局一致性帶來的結果就是數據的鎖定(AT模式也是存在全局鎖的,可是隔離級別沒法保證,後邊咱們會詳細說),例如全局事務中有一條update語句,其餘事務想要更新同一條數據的話,只能等待全局事務結束sql

傳統XA模式是存在一些問題的,Seata也是作了相關的優化,更多關於Seata XA的內容,傳送門? http://seata.io/zh-cn/blog/se...
TCC

image.png

TCC 模式一樣包含兩個階段數據庫

  • Try 階段 :全部參與分佈式事務的分支,對業務資源進行檢查和預留
  • 二階段 Confirm:全部分支的Try所有成功後,執行業務提交
  • 二階段 Cancel:取消Try階段預留的業務資源
對比AT或者XA模式來講,TCC模式須要咱們本身抽象並實現Try,Confirm,Cancel三個接口,編碼量會大一些,可是因爲事務的每個階段都由開發人員自行實現。並且相較於AT模式來講,減小了SQL解析的過程,也沒有全局鎖的限制,因此TCC模式的性能是優於AT 、XA模式。
PS:果真簡單和高效難以兩全的
Saga

image.png

Saga 是長事務解決方案,每一個參與者須要實現事務的正向操做和補償操做。當參與者正向操做執行失敗時,回滾本地事務的同時,會調用上一階段的補償操做,在業務失敗時最終會使事務回到初始狀態springboot

Saga與TCC相似,一樣沒有全局鎖。因爲相比缺乏鎖定資源這一步,在某些適合的場景,Saga要比TCC實現起來更簡單。
因爲Saga和TCC都須要咱們手動編碼實現,因此在開發時咱們須要參考一些設計上的規範,因爲不是本文重點,這裏就很少說了,能夠參考 分佈式事務 Seata 及其三種模式詳解

在咱們瞭解完四種分佈式事務的原理以後,咱們回到本文重點AT模式併發

AT 如何使用

模擬需求:如下訂單爲例,在分佈式的電商場景中,訂單服務和庫存服務多是兩個數據庫框架

咱們先來看看AT模式下的代碼是什麼樣的,這裏忽略了Seata的相關配置,只看業務部分

image.png

在須要開啓分佈式事務的方法上標記@GlobalTransactional,而後執行分別執行扣減庫存和扣減庫存操做的,事務的參與者能夠是本地的數據源,或者RPC的遠程調用(遠程調用的話須要攜帶全局事務ID,也就是上圖的xid)

AT 一階段

以前說過AT模式分爲兩個階段,第一階段包括提交業務數據和回滾日誌(undoLog),第一階段具體流程以下圖

image.png

GlobalTransactional 切面

標記@GlobalTransactional的方法經過AOP實現了,開啓全局事務和提交全局事務兩個操做,與Spring 事務機制相似,當 GlobalTransactionalInterceptor 在事務執行過程當中捕獲到Throwable時,會發起全局事務回滾

0.1 步驟中會生成一個全局事務ID

0.2 全部事務參與者執行結束後,一階段事務提交

undoLog

咱們先來看看 Seata undoLog 的結構

// 省略了相關方法
public class SQLUndoLog {
    // insert, update ...
    private SQLType sqlType;

    private String tableName;

    private TableRecords beforeImage;

    private TableRecords afterImage;
}

Seata 在執行業務SQL先後,會生成beforeImage和afterImage,在須要回滾時,根據SQLType,決定具體的回滾策略,例如SQLType=update時,將數據回滾到beforeImage的狀態,若是SQLType=insert,則根據afterImage刪除數據

如2.4所示,每條業務SQL,執行成功後,會爲這條SQL生成LockKey,格式爲tableName:PrimaryKey

註冊分支事務

在3.1步驟註冊分支事務時,client會把全部的LockKey 拼到一塊兒做爲全局鎖發送給Seata-server。若是註冊成功,寫入undoLog,並提交本地事務,一階段結束,等待二階段反饋

若是當前有其餘分支事務已經持有了相同的鎖(即其餘事務也在處理相同表的同一行),則client 註冊事務分支失敗。client會根據客戶端定義的重發時間和重發次數進行不斷的嘗試,若是重試結束仍然沒有得到鎖,則一階段失敗,本地事務回滾。若是該全局事務存在已經註冊成功分支事務,Seata-server 進行二階段回滾

全局鎖會在分支事務二階段結束後釋放

Seata 全局鎖的設計是爲了什麼?
以扣減庫存場景爲例,TX1 完成庫存扣減的一階段,庫存從100扣減爲99,正在等待二階段的通知。TX2也要扣減同一商品的庫存,若是沒有全局鎖的限制,TX2庫存從99扣減爲98,這時若是TX1接收到回滾通知,進行回滾把庫存從98回滾到100。由於沒有全局鎖,形成了 髒寫

AT 二階段

二階段是徹底異步化的而且徹底由Seata控制,Seata根據全部事務參與者的提交狀況決定二階段如何處理

  • 若是全部事務提交成功,則二階段的任務就是刪除一階段生成 的undoLog,並釋放全局鎖
  • 若是部分事務參與者提交失敗,則須要根據undoLog對已經註冊的事務分支進行回滾,並釋放全局鎖

對Seata提出的疑問

至此咱們已經初步瞭解了Seata的AT模式是如何實現的了

若是你也和我同樣,仔細思考了上述過程,可能會提出一些問題,這邊我列舉一下我在學習Seata時,遇到的問題,以及我得出的結論

問題1. Seata如何作到無侵入的分析業務SQL生成undoLog,註冊事務分支等操做?

Seata 代理了DataSource,咱們能夠經過在代碼注入一個DataSource來驗證個人說法,目前的DataSource 是 io.seata.rm.datasource.DataSourceProxy

image.png

全部的Java持久化框架,最終在操做數據庫時都會經過DataSource接口獲取Connection,經過Connection 實現對數據庫的增刪改查,事務控制。

image.png

Seata 經過代理Connection的方式,作到了無侵入的生成undoLog,註冊事務分支,具體源碼能夠查看io.seata.rm.datasource.ConnectionProxy

問題2. ConnectionProxy 如何判斷當前事務是全局事務,仍是本地事務?

經過當前線程是否綁定了全局事務id,在進行全局事務以前,須要調用RootContext.bind(xid);

問題3. 全局事務併發更新

仍是如下訂單扣減庫存的場景爲例,若是TX1和TX2同時扣減product_id爲1的庫存,這時Seata會不會生成相同的beforeImage?

舉個例子,TX1讀庫存爲100,TX1扣減庫存1,此時BeforeImage爲100
緊接着 若是TX2讀庫存也爲100,那麼就有問題了,無論TX2扣減多少庫存,若是TX1回滾那麼至關於覆蓋了TX2扣減的庫存,出現了髒寫

Seata是如何解決這個問題的?

源碼位置:io.seata.rm.datasource.exec.AbstractDMLBaseExecutor::executeAutoCommitFalse
image.png

能夠看到這裏的邏輯和我上面畫的圖一致,證實我沒有瞎說 ?

咱們來看一下beforeImage(),這是一個抽象方法,看一下他的子類UpdateExecutor是如何實現的

image.png

經過Debug,能夠看出Seata這邊也是確實考慮了這個問題,直接簡單而有效的解決了這個問題

回到咱們的例子,因爲SELECT FOR UPDATE的存在,TX2若是也想讀同一條數據的話,只能等到TX1 提交事務後,才能讀到。因此問題解決

問題4. 全局事務外的更新

咱們如今能夠確認在Seata的保證下,全局事務,不會形成數據的髒寫,可是全局事務外會!

什麼意思呢?

還以庫存爲例

  • 用戶正在搶購,用戶A完成了1階段的庫存扣減,這個時候庫存爲99。
  • 此時庫存管理員上線了,他查了一下庫存爲99。嗯...太少了,我加100個,庫存管理員把庫存更新爲200。
  • 而此時seata給用戶A生成beforeImage爲100,若是此時用戶A的全局事務失敗了,發生了回滾,再次將庫存更新爲100... 再次出現髒寫

Seata 針對這個問題,提供了@GlobalLock註解,標記該註解時,會像全局事務同樣進行SQL分析,競爭全局鎖,就不會出現上述問題了

關於這個問題能夠參考Seata的FAQ文檔 http://seata.io/zh-cn/docs/ov...

問題5. @GlobalTransactional 和 @Transactional 同時使用會怎麼樣

咱們上文中已經說過了 @GlobalTransactional 的做用了,他是負責開啓全局事務/提交事務1階段,說白了@GlobalTransactional 只和Seata-server 交互,而 @Transactional 管理的是本地數據庫的事務,因此兩者不發生衝突。

可是須要注意 @GlobalTransactional AOP 覆蓋範圍必定要大於 @Transactional

問題6. 若是其中某一個事務分支超時未提交,會發生什麼

這個我並無看源碼,而是經過跑demo,驗證的

例如如今有A,B兩個事務分支

  • A 正常提交,並向Seata註冊分支成功
  • B 2分鐘後提交事務,並向Seata發起註冊

Seata的全局事務超時時間,默認是1分鐘,Seata-server 在檢測到有超時的全局事務時,會向全部已提交的分支,發起回滾。而超時提交的事務,向Seata-server發起分支註冊時,響應結果爲事務已超時,或者事務不存在,也會回滾本地事務

問題7. Seata-client 如何接收Seata-server發起的通知

Seata-client 包含了Netty服務,在啓動時Netty會監聽端口,並向Seata-server 發起註冊。server中存儲了client 的調用地址。

總結

咱們學習了Seata的AT模式是如何工做的,能夠看出Seata模式在開發上是很是簡單的,可是Seata的背後爲了維持分佈式事務的數據一致性,作了大量的工做,AT模式很是適合現有的業務模型直接遷移。

可是他的缺點也很明顯,性能並非那麼的優秀。例如咱們剛剛看到的全局鎖的問題,爲了數據不會發生髒寫,Seata犧牲了業務的併發能力。在很是要求性能的場景,可能仍是須要考慮TCC,SAGA,可靠消息等方案

在使用Seata開發前,建議你們先去閱讀一下FAQ文檔,避免踩坑 https://seata.io/zh-cn/docs/o...

DEMO

https://github.com/TavenYin/t...

參考

相關文章
相關標籤/搜索