Spring Cloud Seata系列:基於AT模式實現分散式事務

Code技術分享發表於2023-12-13


https://seata.io/zh-cn/docs/dev/mode/at-mode

AT模式同樣是分階段提交的事務模型,不過缺彌補了XA模型中資源鎖定週期過長的缺陷。

前提

  • 基於支援本地 ACID 事務的關係型資料庫。
  • Java 應用,透過 JDBC 訪問資料庫

Seata的AT模型

  • 一階段:業務資料和回滾日誌記錄在同一個本地事務中提交,釋放本地鎖和連線資源。
  • 二階段:
    • 提交非同步化,非常快速地完成。
    • 回滾透過一階段的回滾日誌進行反向補償。

基本流程圖:

image

階段一RM的工作:

  • 註冊分支事務
  • 記錄undo-log(資料快照)
  • 執行業務sql並提交
  • 報告事務狀態

階段二提交時RM的工作:

  • 刪除undo-log即可

階段二回滾時RM的工作:

  • 根據undo-log恢復資料到更新前

流程梳理

我們用一個真實的業務來梳理下AT模式的原理。

比如,現在又一個資料庫表,記錄使用者餘額:

id money
1 100

其中一個分支業務要執行的SQL為:

update tb_account set money = money - 10 where id = 1

AT模式下,當前分支事務執行流程如下:

一階段:

1)解析 SQL:得到 SQL 的型別(UPDATE),表(tb_account),條件(where id = 1)等相關的資訊

2)查詢前映象:根據解析得到的條件資訊,生成查詢語句,定位資料

select id,money from tb_account where id = 1;

得到前映象:

id money
1 100

3)執行業務 SQL:更新這條記錄的 money 為 90

4)查詢後映象:根據前映象的結果,透過 主鍵 定位資料。

select id,money from tb_account where id = 1;

得到後映象:

id money
1 90

5)插入回滾日誌:把前後映象資料以及業務 SQL 相關的資訊組成一條回滾日誌記錄,插入到 UNDO_LOG 表中

{
    "branchId": 641789253,
    "undoItems": [{
        "afterImage": {
            "rows":[{
                "fields": [{
                    "name": "id",
                    "type": 4,
                    "value": 1
                }, {
                    "name": "moneny",
                    "type": 12,
                    "value": "90"
                }]
            }],
          "tableName": "tb_account"
        },
        "beforeImage": {
            "rows": [{
                "fields": [{
                    "name": "id",
                    "type": 4,
                    "value": 1
                }, {
                    "name": "moneny",
                    "type": 12,
                    "value": "100"
                }]
            }],
            "tableName": "tb_account"
        },
        "sqlType": "UPDATE"
    }],
    "xid": "xid:xxx"
}

6)提交前,向 TC 註冊分支:申請 tb_account 表中,主鍵值等於 1 的記錄的 全域性鎖

7)本地事務提交:業務資料的更新和前面步驟中生成的 UNDO LOG 一併提交。

8)將本地事務提交的結果上報給 TC

二階段-回滾

  1. 收到 TC 的分支回滾請求,開啟一個本地事務,執行如下操作。
  2. 透過 XID 和 Branch ID 查詢到相應的 UNDO LOG 記錄。
  3. 資料校驗:拿 UNDO LOG 中的後鏡與當前資料進行比較,如果有不同,說明資料被當前全域性事務之外的動作做了修改。這種情況,需要根據配置策略來做處理,詳細的說明在另外的文件中介紹。
  4. 根據 UNDO LOG 中的前映象和業務 SQL 的相關資訊生成並執行回滾的語句:
update tb_account set money = 100 where id = 1;
  1. 提交本地事務。並把本地事務的執行結果(即分支事務回滾的結果)上報給 TC。

二階段-提交

  1. 收到 TC 的分支提交請求,把請求放入一個非同步任務的佇列中,馬上返回提交成功的結果給 TC。

  2. 非同步任務階段的分支提交請求將非同步和批次地刪除相應 UNDO LOG 記錄。

流程圖:

image

髒寫問題

在多執行緒併發訪問AT模式的分散式事務時,有可能出現髒寫問題,如圖:

image

解決思路就是引入了全域性鎖的概念。在釋放DB鎖之前,先拿到全域性鎖。避免同一時刻有另外一個事務來操作當前資料。

image

寫隔離

  • 一階段:業務資料和回滾日誌記錄在同一個本地事務中提交,釋放本地鎖和連線資源。
  • 二階段:
    • 提交非同步化,非常快速地完成。
    • 回滾透過一階段的回滾日誌進行反向補償。

以一個示例來說明:

兩個全域性事務 tx1 和 tx2,分別對 a 表的 m 欄位進行更新操作,m 的初始值 1000。

tx1 先開始,開啟本地事務,拿到本地鎖,更新操作 m = 1000 - 100 = 900。本地事務提交前,先拿到該記錄的 全域性鎖 ,本地提交釋放本地鎖。 tx2 後開始,開啟本地事務,拿到本地鎖,更新操作 m = 900 - 100 = 800。本地事務提交前,嘗試拿該記錄的 全域性鎖 ,tx1 全域性提交前,該記錄的全域性鎖被 tx1 持有,tx2 需要重試等待 全域性鎖

image

tx1 二階段全域性提交,釋放 全域性鎖 。tx2 拿到 全域性鎖 提交本地事務。

image

如果 tx1 的二階段全域性回滾,則 tx1 需要重新獲取該資料的本地鎖,進行反向補償的更新操作,實現分支的回滾。

此時,如果 tx2 仍在等待該資料的 全域性鎖,同時持有本地鎖,則 tx1 的分支回滾會失敗。分支的回滾會一直重試,直到 tx2 的 全域性鎖 等鎖超時,放棄 全域性鎖 並回滾本地事務釋放本地鎖,tx1 的分支回滾最終成功。

因為整個過程 全域性鎖 在 tx1 結束前一直是被 tx1 持有的,所以不會發生 髒寫 的問題。

讀隔離

在資料庫本地事務隔離級別 讀已提交(Read Committed) 或以上的基礎上,Seata(AT 模式)的預設全域性隔離級別是 讀未提交(Read Uncommitted)

如果應用在特定場景下,必需要求全域性的 讀已提交 ,目前 Seata 的方式是透過 SELECT FOR UPDATE 語句的代理。

image

SELECT FOR UPDATE 語句的執行會申請 全域性鎖 ,如果 全域性鎖 被其他事務持有,則釋放本地鎖(回滾 SELECT FOR UPDATE 語句的本地執行)並重試。這個過程中,查詢是被 block 住的,直到 全域性鎖 拿到,即讀取的相關資料是 已提交 的,才返回。

出於總體效能上的考慮,Seata 目前的方案並沒有對所有 SELECT 語句都進行代理,僅針對 FOR UPDATE 的 SELECT 語句。

優缺點

AT模式的優點:

  • 一階段完成直接提交事務,釋放資料庫資源,效能比較好
  • 利用全域性鎖實現讀寫隔離
  • 沒有程式碼侵入,框架自動完成回滾和提交

AT模式的缺點:

  • 兩階段之間屬於軟狀態,屬於最終一致
  • 框架的快照功能會影響效能,但比XA模式要好很多

AT與XA的區別

簡述AT模式與XA模式最大的區別是什麼?

  • XA模式一階段不提交事務,鎖定資源;AT模式一階段直接提交,不鎖定資源。
  • XA模式依賴資料庫機制實現回滾;AT模式利用資料快照實現資料回滾。
  • XA模式強一致;AT模式最終一致

實現AT模式

AT模式中的快照生成、回滾等動作都是由框架自動完成,沒有任何程式碼侵入,因此實現非常簡單。

只不過,AT模式需要一個表來記錄全域性鎖、另一張表來記錄資料快照undo_log。

1)匯入資料庫表,記錄全域性鎖

匯入undo_log表匯入到微服務關聯的資料庫:

https://github.com/seata/seata/blob/2.x/script/client/at/db/mysql.sql

-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);

2)修改application.yml檔案(每個參與事務的微服務),預設AT模式,開啟AT模式:

seata:
  enabled: true
  tx-service-group: default_tx_group # 事務組名稱
  service:
    vgroup-mapping:
      default_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
  data-source-proxy-mode: AT

3)給發起全域性事務的入口方法新增@GlobalTransactional註解:

本例中是OrderServiceImpl中的create方法.

image

相關文章