Seata之小結和測試

雩娄的木子發表於2024-11-29

目錄
  • 一、本地事務
  • 二、分散式事務
    • 2.1、典型的分散式事務應用場景
    • 2.2、跨庫事務
    • 2.3、分庫分表
    • 2.4、微服務架構
  • 三、兩階段提交協議(2PC)
    • 2PC存在的問題
  • 四、Seata
    • 4.1、Seata的三大角色
    • 4.2、Seata AT模式的設計思路
  • 五、Seata快速開始
    • Seata Server(TC)環境搭建
      • 步驟一:下載安裝包
      • 步驟二:建表(db模式)
      • 步驟三:配置Nacos註冊中心
      • 步驟四:配置Nacos配置中心
      • 步驟五:啟動Seata Server
    • Seata Client快速開始
      • 1) 環境準備
      • 2) 微服務匯入seata依賴
      • 3)微服務對應資料庫中新增undo_log表(僅AT模式)
      • 4) 微服務application.yml中新增seata配置
      • 5) 在全域性事務發起者中新增@GlobalTransactional註解
      • 6)測試分散式事務是否生效

一、本地事務

大多數場景下,我們的應用都只需要操作單一的資料庫,這種情況下的事務稱之為本地事務(Local Transaction)。本地事務的ACID特性是資料庫直接提供支援。本地事務應用架構如下所示:

在JDBC程式設計中,我們透過java.sql.Connection物件來開啟、關閉或者提交事務。程式碼如下所示:

Connection conn = ... //獲取資料庫連線
conn.setAutoCommit(false); //開啟事務
try{
   //...執行增刪改查sql
   conn.commit(); //提交事務
}catch (Exception e) {
  conn.rollback();//事務回滾
}finally{
   conn.close();//關閉連結
}

二、分散式事務

在微服務架構中,完成某一個業務功能可能需要橫跨多個服務,操作多個資料庫。這就涉及到到了分散式事務,需要操作的資源位於多個資源伺服器上,而應用需要保證對於多個資源伺服器的資料操作,要麼全部成功,要麼全部失敗。本質上來說,分散式事務就是為了保證不同資源伺服器的資料一致性。

2.1、典型的分散式事務應用場景

2.2、跨庫事務

跨庫事務指的是,一個應用某個功能需要操作多個庫,不同的庫中儲存不同的業務資料。下圖演示了一個服務同時操作2個庫的情況:

2.3、分庫分表

通常一個庫資料量比較大或者預期未來的資料量比較大,都會進行分庫分表。如下圖,將資料庫B拆分成了2個庫:

對於分庫分表的情況,一般開發人員都會使用一些資料庫中介軟體來降低sql操作的複雜性。如,對於sql:insert into user(id,name) values (1,"張三"),(2,"李四")。這條sql是操作單庫的語法,單庫情況下,可以保證事務的一致性。 但是由於現在進行了分庫分表,開發人員希望將1號記錄插入分庫1,2號記錄插入分庫2。所以資料庫中介軟體要將其改寫為2條sql,分別插入兩個不同的分庫,此時要保證兩個庫要不都成功,要不都失敗,因此基本上所有的資料庫中介軟體都面臨著分散式事務的問題。

2.4、微服務架構

下圖演示了一個3個服務之間彼此呼叫的微服務架構:

Service A完成某個功能需要直接運算元據庫,同時需要呼叫Service B和Service C,而Service B又同時操作了2個資料庫,Service C也操作了一個庫。需要保證這些跨服務呼叫對多個資料庫的操作要麼都成功,要麼都失敗,實際上這可能是最典型的分散式事務場景。

小結:上述討論的分散式事務場景中,無一例外的都直接或者間接的操作了多個資料庫。如何保證事務的ACID特性,對於分散式事務實現方案而言,是非常大的挑戰。同時,分散式事務實現方案還必須要考慮效能的問題,如果為了嚴格保證ACID特性,導致效能嚴重下降,那麼對於一些要求快速響應的業務,是無法接受的。

三、兩階段提交協議(2PC)

兩階段提交(Two Phase Commit),就是將提交(commit)過程劃分為2個階段(Phase):

階段1:

TM通知各個RM準備提交它們的事務分支。如果RM判斷自己進行的工作可以被提交,那就對工作內容進行持久化,再給TM肯定答覆;要是發生了其他情況,那給TM的都是否定答覆。

以mysql資料庫為例,在第一階段,事務管理器向所有涉及到的資料庫伺服器發出prepare"準備提交"請求,資料庫收到請求後執行資料修改和日誌記錄等處理,處理完成後只是把事務的狀態改成"可以提交",然後把結果返回給事務管理器。

階段2

TM根據階段1各個RM prepare的結果,決定是提交還是回滾事務。如果所有的RM都prepare成功,那麼TM通知所有的RM進行提交;如果有RM prepare失敗的話,則TM通知所有RM回滾自己的事務分支。

以mysql資料庫為例,如果第一階段中所有資料庫都prepare成功,那麼事務管理器向資料庫伺服器發出"確認提交"請求,資料庫伺服器把事務的"可以提交"狀態改為"提交完成"狀態,然後返回應答。如果在第一階段內有任何一個資料庫的操作發生了錯誤,或者事務管理器收不到某個資料庫的回應,則認為事務失敗,回撤所有資料庫的事務。資料庫伺服器收不到第二階段的確認提交請求,也會把"可以提交"的事務回撤。

2PC存在的問題

  • 同步阻塞問題

2PC 中的參與者是阻塞的。在第一階段收到請求後就會預先鎖定資源,一直到 commit 後才會釋放。

  • 單點故障

由於協調者的重要性,一旦協調者TM發生故障,參與者RM會一直阻塞下去。尤其在第二階段,協調者發生故障,那麼所有的參與者還都處於鎖定事務資源的狀態中,而無法繼續完成事務操作。

  • 資料不一致

若協調者第二階段傳送提交請求時崩潰,可能部分參與者收到commit請求提交了事務,而另一部分參與者未收到commit請求而放棄事務,從而造成資料不一致的問題。

四、Seata

Seata 是一款開源的分散式事務解決方案,致力於提供高效能和簡單易用的分散式事務服務。Seata 將為使用者提供了 AT、TCC、SAGA 和 XA 事務模式,為使用者打造一站式的分散式解決方案。AT模式是阿里首推的模式,阿里雲上有商用版本的GTS(Global Transaction Service 全域性事務服務)

官網:https://seata.io/zh-cn/index.html

4.1、Seata的三大角色

在 Seata 的架構中,一共有三個角色:

  • TC (Transaction Coordinator) - 事務協調者

維護全域性和分支事務的狀態,驅動全域性事務提交或回滾。

  • TM (Transaction Manager) - 事務管理器

定義全域性事務的範圍:開始全域性事務、提交或回滾全域性事務。

  • RM (Resource Manager) - 資源管理器

管理分支事務處理的資源,與TC交談以註冊分支事務和報告分支事務的狀態,並驅動分支事務提交或回滾。

其中,TC 為單獨部署的 Server 服務端,TM 和 RM 為嵌入到應用中的 Client 客戶端。

在 Seata 中,一個分散式事務的生命週期如下:

  1. TM 請求 TC 開啟一個全域性事務。TC 會生成一個 XID 作為該全域性事務的編號。XID會在微服務的呼叫鏈路中傳播,保證將多個微服務的子事務關聯在一起。
  2. RM 請求 TC 將本地事務註冊為全域性事務的分支事務,透過全域性事務的 XID 進行關聯。
  3. TM 請求 TC 告訴 XID 對應的全域性事務是進行提交還是回滾。
  4. TC 驅動 RM 們將 XID 對應的自己的本地事務進行提交還是回滾。

思考

1、為什麼是TM告知TC需要回滾或者是提交?

2、TC如何告知RM進行回滾或者是提交?

3、RM如何進行提交或者是回滾?

在下面將會對這些問題進行一一分析和解決,但不會深入到原始碼級別。

4.2、Seata AT模式的設計思路

Seata AT模式的核心是對業務無侵入,是一種改進後的兩階段提交,其設計思路如下:

  • 一階段:業務資料和回滾日誌記錄在同一個本地事務中提交,釋放本地鎖和連線資源。

  • 二階段:

    • 提交非同步化,非常快速地完成。

    • 回滾透過一階段的回滾日誌進行反向補償。

一階段

業務資料和回滾日誌記錄在同一個本地事務中提交,釋放本地鎖和連線資源。

核心在於對業務sql進行解析,轉換成undolog,並同時入庫,這是怎麼做的呢?

二階段

  • 分散式事務操作成功,則TC通知RM非同步刪除undolog

  • 分散式事務操作失敗,TM向TC傳送回滾請求,RM 收到協調器TC發來的回滾請求,透過 XID 和 Branch ID 找到相應的回滾日誌記錄,透過回滾記錄生成反向的更新 SQL 並執行,以完成分支的回滾。

五、Seata快速開始

Seata分TC、TM和RM三個角色,TC(Server端)為單獨服務端部署,TM和RM(Client端)由業務系統整合。

Seata Server(TC)環境搭建

Server端儲存模式(store.mode)支援三種:

  • file:單機模式,全域性事務會話資訊記憶體中讀寫並持久化本地檔案root.data,效能較高
  • db:高可用模式,全域性事務會話資訊透過db共享,相應效能差些
  • redis:1.3及以上版本支援,效能較高,存在事務資訊丟失風險,請提前配置適合當前場景的redis持久化配置

資源目錄:

  • https://github.com/seata/seata/tree/v1.5.1/script

  • client

    • 存放client端sql指令碼,引數配置
  • config-center

    • 各個配置中心引數匯入指令碼,config.txt(包含server和client)為通用引數檔案
  • server

    • server端資料庫指令碼及各個容器配置

db儲存模式+Nacos(註冊&配置中心)方式部署

步驟一:下載安裝包

https://github.com/seata/seata/releases

步驟二:建表(db模式)

建立資料庫seata,執行sql指令碼,https://github.com/seata/seata/tree/v1.5.1/script/server/db

步驟三:配置Nacos註冊中心

註冊中心可以說是微服務架構中的”通訊錄“,它記錄了服務和服務地址的對映關係。在分散式架構中,服務會註冊到註冊中心,當服務需要呼叫其它服務時,就到註冊中心找到服務的地址,進行呼叫。比如Seata Client端(TM,RM),發現Seata Server(TC)叢集的地址,彼此通訊。

注意:Seata的註冊中心是作用於Seata自身的,和Spring Cloud的註冊中心無關

Seata支援哪些註冊中心?

  1. eureka
  2. consul
  3. nacos
  4. etcd
  5. zookeeper
  6. sofa
  7. redis
  8. file (直連)

配置將Seata Server註冊到Nacos,修改conf/application.yml檔案

registry:
    # support: nacos, eureka, redis, zk, consul, etcd3, sofa
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace:
      cluster: default
      username: nacos
      password: nacos

注意:請確保client與server的註冊處於同一個namespace和group,不然會找不到服務。

啟動 Seata-Server 後,會發現Server端的服務出現在 Nacos 控制檯中的註冊中心列表中。

步驟四:配置Nacos配置中心

配置中心可以說是一個"大貨倉",內部放置著各種配置檔案,你可以透過自己所需進行獲取配置載入到對應的客戶端。比如Seata Client端(TM,RM),Seata Server(TC),會去讀取全域性事務開關,事務會話儲存模式等資訊。

注意:Seata的配置中心是作用於Seata自身的,和Spring Cloud的配置中心無關

Seata支援哪些配置中心?

  1. nacos
  2. consul
  3. apollo
  4. etcd
  5. zookeeper
  6. file (讀本地檔案, 包含conf、properties、yml配置檔案的支援)

1)配置Nacos配置中心地址,修改conf/application.yml檔案

seata:
  config:
    # support: nacos, consul, apollo, zk, etcd3
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: 7e838c12-8554-4231-82d5-6d93573ddf32
      group: SEATA_GROUP
      data-id: seataServer.properties
      username: nacos
      password: nacos

2)上傳配置至Nacos配置中心

https://github.com/seata/seata/tree/v1.5.1/script/config-center

a) 獲取/seata/script/config-center/config.txt,修改為db儲存模式,並修改mysql連線配置

store.mode=db
store.lock.mode=db
store.session.mode=db
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=root

在store.mode=db,由於seata是透過jdbc的executeBatch來批次插入全域性鎖的,根據MySQL官網的說明,連線引數中的rewriteBatchedStatements為true時,在執行executeBatch,並且操作型別為insert時,jdbc驅動會把對應的SQL最佳化成insert into () values (), ()的形式來提升批次插入的效能。 根據實際的測試,該引數設定為true後,對應的批次插入效能為原來的10倍多,因此在資料來源為MySQL時,建議把該引數設定為true。

b) 配置事務分組, 要與client配置的事務分組一致

  • 事務分組:seata的資源邏輯,可以按微服務的需要,在應用程式(客戶端)對自行定義事務分組,每組取一個名字。
  • 叢集:seata-server服務端一個或多個節點組成的叢集cluster。 應用程式(客戶端)使用時需要指定事務邏輯分組與Seata服務端叢集的對映關係。

事務分組如何找到後端Seata叢集(TC)?

  1. 首先應用程式(客戶端)中配置了事務分組(GlobalTransactionScanner 構造方法的txServiceGroup引數)。若應用程式是SpringBoot則透過seata.tx-service-group 配置。
  2. 應用程式(客戶端)會透過使用者配置的配置中心去尋找service.vgroupMapping .[事務分組配置項],取得配置項的值就是TC叢集的名稱。若應用程式是SpringBoot則透過seata.service.vgroup-mapping.事務分組名=叢集名稱 配置
  3. 拿到叢集名稱程式透過一定的前字尾+叢集名稱去構造服務名,各配置中心的服務名實現不同(前提是Seata-Server已經完成服務註冊,且Seata-Server向註冊中心報告cluster名與應用程式(客戶端)配置的叢集名稱一致)
  4. 拿到服務名去相應的註冊中心去拉取相應服務名的服務列表,獲得後端真實的TC服務列表(即Seata-Server叢集節點列表)

c) 在nacos配置中心中新建配置,dataId為seataServer.properties,配置內容為上面修改後的config.txt中的配置資訊

從v1.4.2版本開始,seata已支援從一個Nacos dataId中獲取所有配置資訊,你只需要額外新增一個dataId配置項。

新增後檢視:

步驟五:啟動Seata Server

啟動命令:

bin/seata-server.sh 

啟動成功,檢視控制檯,賬號密碼都是seata。http://localhost:7091/#/login

在Nacos註冊中心中可以檢視到seata-server註冊成功

支援的啟動引數

引數 全寫 作用 備註
-h --host 指定在註冊中心註冊的 IP 不指定時獲取當前的 IP,外部訪問部署在雲環境和容器中的 server 建議指定
-p --port 指定 server 啟動的埠 預設為 8091
-m --storeMode 事務日誌儲存方式 支援file,db,redis,預設為 file 注:redis需seata-server 1.3版本及以上
-n --serverNode 用於指定seata-server節點ID 如 1,2,3..., 預設為 1
-e --seataEnv 指定 seata-server 執行環境 如 dev, test 等, 服務啟動時會使用 registry-dev.conf 這樣的配置

比如:

bin/seata-server.sh -p 8091 -h 127.0.0.1 -m db

Seata Client快速開始

Spring Cloud Alibaba整合Seata AT模式實戰

業務場景

使用者下單,整個業務邏輯由三個微服務構成:

  • 庫存服務:對給定的商品扣除庫存數量。
  • 訂單服務:根據採購需求建立訂單。
  • 帳戶服務:從使用者帳戶中扣除餘額。

1) 環境準備

  • 父pom指定微服務版本
Spring Cloud Alibaba Version Spring Cloud Version Spring Boot Version Seata Version
2.2.8.RELEASE Spring Cloud Hoxton.SR12 2.3.12.RELEASE 1.5.1
  • 啟動Seata Server(TC)端,Seata Server使用nacos作為配置中心和註冊中心
  • 啟動nacos服務

2) 微服務匯入seata依賴

spring-cloud-starter-alibaba-seata內部整合了seata,並實現了xid傳遞

<!-- seata-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

3)微服務對應資料庫中新增undo_log表(僅AT模式)

https://github.com/seata/seata/blob/v1.5.1/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';

4) 微服務application.yml中新增seata配置

seata:
  application-id: ${spring.application.name}
  # seata 服務分組,要與服務端配置service.vgroup_mapping的字尾對應
  tx-service-group: default_tx_group
  registry:
    # 指定nacos作為註冊中心
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      namespace: public
      group: SEATA_GROUP

  config:
    # 指定nacos作為配置中心
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: 7e838c12-8554-4231-82d5-6d93573ddf32
      group: SEATA_GROUP
      data-id: seataServer.properties

注意:請確保client與server的註冊中心和配置中心namespace和group一致

5) 在全域性事務發起者中新增@GlobalTransactional註解

核心程式碼

@Override
@GlobalTransactional(name="createOrder",rollbackFor=Exception.class)
public Order saveOrder(OrderVo orderVo){
    log.info("=============使用者下單=================");
    log.info("當前 XID: {}", RootContext.getXID());
    
    // 儲存訂單
    Order order = new Order();
    order.setUserId(orderVo.getUserId());
    order.setCommodityCode(orderVo.getCommodityCode());
    order.setCount(orderVo.getCount());
    order.setMoney(orderVo.getMoney());
    order.setStatus(OrderStatus.INIT.getValue());

    Integer saveOrderRecord = orderMapper.insert(order);
    log.info("儲存訂單{}", saveOrderRecord > 0 ? "成功" : "失敗");
    
    //扣減庫存
    storageFeignService.deduct(orderVo.getCommodityCode(),orderVo.getCount());
    
    //扣減餘額
    accountFeignService.debit(orderVo.getUserId(),orderVo.getMoney());

    //更新訂單
    Integer updateOrderRecord = orderMapper.updateOrderStatus(order.getId(),OrderStatus.SUCCESS.getValue());
    log.info("更新訂單id:{} {}", order.getId(), updateOrderRecord > 0 ? "成功" : "失敗");
    
    return order;
    
}

6)測試分散式事務是否生效

  • 分散式事務成功,模擬正常下單、扣庫存,扣餘額
  • 分散式事務失敗,模擬下單扣庫存成功、扣餘額失敗,事務是否回滾

相關文章