Alibaba微服務組件 - Seata(二) 分布式事務Seata使用


2.1 Seata 是什么

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

官網:https://seata.io/zh-cn/index.html
源碼: https://github.com/seata/seata
官方Demo: https://github.com/seata/seata-samples
seata版本:v1.3.0

2.1.1 Seata的三大角色

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

  • TC (Transaction Coordinator) - 事務協調者
    維護全局和分支事務的狀態,驅動全局事務提交或回滾。
  • TM (Transaction Manager) - 事務管理器
    定義全局事務的范圍:開始全局事務、提交或回滾全局事務。
  • RM (Resource Manager) - 資源管理器
    管理分支事務處理的資源,與TC交談以注冊分支事務和報告分支事務的狀態,並驅動分支事務提交或回滾。
    其中,TC 為單獨部署的 Server 服務端,TM 和 RM 為嵌入到應用中的 Client 客戶端。

在 Seata 中,一個分布式事務的生命周期如下:
image

  1. TM 請求 TC 開啟一個全局事務。TC 會生成一個 XID 作為該全局事務的編號。XID,會在微服務的調用鏈路中傳播,保證將多個微服務的子事務關聯在一起。
    當一進入事務方法中就會生成XID , global_table 就是存儲的全局事務信息 ,
  2. RM 請求 TC 將本地事務注冊為全局事務的分支事務,通過全局事務的 XID 進行關聯。
    當運行數據庫操作方法,branch_table 存儲事務參與者
  3. TM 請求 TC 告訴 XID 對應的全局事務是進行提交還是回滾。
  4. TC 驅動 RM 們將 XID 對應的自己的本地事務進行提交還是回滾。
    image

2.1.2 設計思路

AT模式的核心是對業務無侵入,是一種改進后的兩階段提交,其設計思路如圖
第一階段:
業務數據和回滾日志記錄在同一個本地事務中提交,釋放本地鎖和連接資源。核心在於對業務sql進行解析,轉換成undolog,並同時入庫,這是怎么做的呢?先拋出一個概念DataSourceProxy代理數據源,通過名字大家大概也能基本猜到是什么個操作,后面做具體分析。
參考官方文檔: https://seata.io/zh-cn/docs/dev/mode/at-mode.html
image

第二階段:
分布式事務操作成功,則TC通知RM異步刪除undolog
image

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

整體執行流程
image

2.1.3 設計亮點

相比與其它分布式事務框架,Seata架構的亮點主要有幾個:
1. 應用層基於SQL解析實現了自動補償,從而最大程度的降低業務侵入性;
2. 將分布式事務中TC(事務協調者)獨立部署,負責事務的注冊、回滾;
3. 通過全局鎖實現了寫隔離與讀隔離。

2.1.4 存在的問題

  • 性能損耗
    一條Update的SQL,則需要全局事務xid獲取(與TC通訊)、before image(解析SQL,查詢一次數據庫)、after image(查詢一次數據庫)、insert undo log(寫一次數據庫)、before commit(與TC通訊,判斷鎖沖突),這些操作都需要一次遠程通訊RPC,而且是同步的。另外undo log寫入時blob字段的插入性能也是不高的。每條寫SQL都會增加這么多開銷,粗略估計會增加5倍響應時間。
  • 性價比
    為了進行自動補償,需要對所有交易生成前后鏡像並持久化,可是在實際業務場景下,這個是成功率有多高,或者說分布式事務失敗需要回滾的有多少比率?按照二八原則預估,為了20%的交易回滾,需要將80%的成功交易的響應時間增加5倍,這樣的代價相比於讓應用開發一個補償交易是否是值得?
  • 全局鎖
  • 熱點數據
    相比XA,Seata 雖然在一階段成功后會釋放數據庫鎖,但一階段在commit前全局鎖的判定也拉長了對數據鎖的占有時間,這個開銷比XA的prepare低多少需要根據實際業務場景進行測試。全局鎖的引入實現了隔離性,但帶來的問題就是阻塞,降低並發性,尤其是熱點數據,這個問題會更加嚴重。回滾鎖釋放時間Seata在回滾時,需要先刪除各節點的undo log,然后才能釋放TC內存中的鎖,所以如果第二階段是回滾,釋放鎖的時間會更長。
  • 死鎖問題
    Seata的引入全局鎖會額外增加死鎖的風險,但如果出現死鎖,會不斷進行重試,最后靠等待全局鎖超時,這種方式並不優雅,也延長了對數據庫鎖的占有時間。

2.2 Seata快速開始

https://seata.io/zh-cn/docs/ops/deploy-guide-beginner.html

2.2.1 步驟一:下載安裝包

https://github.com/seata/seata/releases
Server端存儲模式(store.mode)支持三種:

  • file:(默認)單機模式,全局事務會話信息內存中讀寫並持久化本地文件root.data,性能較高(默認)
  • db:(5.7+)高可用模式,全局事務會話信息通過db共享,相應性能差些
  •  store {
       mode = "db"
       db {
         datasource = "druid"
         ## mysql/oracle/postgresql/h2/oceanbase etc.
         dbType = "mysql"
         driverClassName = "com.mysql.jdbc.Driver"
         url = "jdbc:mysql://127.0.0.1:3306/seata"
         user = "root"
         password = "123456"
         minConn = 5
         maxConn = 30
         globalTable = "global_table"
         branchTable = "branch_table"
         lockTable = "lock_table"
         queryLimit = 100
         maxWait = 5000
       }
     }
    
    • redis:Seata-Server 1.3及以上版本支持,性能較高,存在事務信息丟失風險,請提前配置適合當前場景的redis持久化配置

資源目錄:https://github.com/seata/seata/tree/1.3.0/script

  • client
    存放client端sql腳本,參數配置
  • config-center
    各個配置中心參數導入腳本,config.txt(包含server和client,原名nacos-config.txt)為通用參數文件
  • server
    server端數據庫腳本及各個容器配置

2.2.2 步驟二:db存儲模式+Nacos(注冊&配置中心)部署

配置Nacos注冊中心    負責事務參與者(微服務) 和TC通信

將Seata Server注冊到Nacos,修改conf目錄下的registry.conf配置
image

然后啟動注冊中心Nacos Server

#進入Nacos安裝目錄,linux單機啟動
bin/startup.sh ‐m standalone
# windows單機啟動
bin/startup.bat

配置Nacos配置中心

image

注意:如果配置了seata server使用nacos作為配置中心,則配置信息會從nacos讀取,file.conf可以不用配置。 客戶端配置registry.conf
使用nacos時也要注意group要和seata server中的group一致,默認group是"DEFAULT_GROUP"

獲取/seata/script/config-center/config.txt,修改配置信息
image

配置事務分組, 要與客戶端配置的事務分組一致
my_test_tx_group需要與客戶端保持一致  default需要跟客戶端和registry.conf中registry中的cluster保持一致(客戶端properties配置:spring.cloud.alibaba.seata.tx‐service‐group=my_test_tx_group)

事務分組:  異地機房停電容錯機制
my_test_tx_group 可以自定義  比如:(guangzhou、shanghai...) , 對應的client也要去設置
seata.service.vgroup‐mapping.projectA=guangzhoudefault
必須要等於 registry.confi  cluster = "default"
image

配置參數同步到Nacos

執行下面shell:

sh ${SEATAPATH}/script/config‐center/nacos/nacos‐config.sh ‐h localhost ‐p 8848 ‐g SEATA_GROUP ‐t 5a3c7d6c‐f497‐ 4d68‐a71a‐2e5e3340b3ca

參數說明:
-h: host,默認值 localhost
-p: port,默認值 8848
-g: 配置分組,默認值為 'SEATA_GROUP'
-t: 租戶信息,對應 Nacos 的命名空間ID字段, 默認值為空 ''
image

精簡配置

service.vgroupMapping.my_test_tx_group=default
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false
store.mode=db
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true
store.db.user=root
store.db.password=root
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000

配置寫入成功:

image

2.2.3 步驟三:啟動Seata Server

  • 源碼啟動: 執行server模塊下io.seata.server.Server.java的main方法
  • 命令啟動: bin/seata-server.sh -h 127.0.0.1 -p 8091 -m db -n 1 -e test
    image

集群啟動Seata Server

bin/seata‐server.sh ‐p 8091 ‐n 1

bin/seata‐server.sh ‐p 8092 ‐n 2

bin/seata‐server.sh ‐p 8093 ‐n 3

啟動成功,默認端口8091

image

在注冊中心中可以查看到seata-server注冊成功

image

2.3 Seata Client快速開始

聲明式事務實現(@GlobalTransactional)

接入微服務應用

業務場景:
用戶下單,整個業務邏輯由三個微服務構成:

  • 訂單服務:根據采購需求創建訂單。
  • 庫存服務:對給定的商品扣除庫存數量。

2.3.1 啟動Seata server端,Seata server使用nacos作為配置中心和注冊中心

(上一步已完成)

2.3.2 配置微服務整合seata

1. 添加pom依賴

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

2. 各微服務對應數據庫中添加undo_log表

CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8;

3. 修改register.conf,配置nacos作為registry.type&config.type,對應seata server也使用nacos

注意:需要指定group = "SEATA_GROUP",因為Seata Server端指定了group = "SEATA_GROUP" ,必須保證一致

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "nacos"

  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
  }
}

如果出現這種問題:
image
一般大多數情況下都是因為配置不匹配導致的:

  • 檢查現在使用的seata服務和項目maven中seata的版本是否一致
  • 檢查tx-service-group,nacos.cluster,nacos.group參數是否和Seata Server中的配置一致

跟蹤源碼:seata/discover包下實現了RegistryService#lookup,用來獲取服務列表

NacosRegistryServiceImpl#lookup
String clusterName = getServiceGroup(key); #獲取seata server集群名稱
List<Instance> firstAllInstances = getNamingInstance().getAllInstances(getServiceName(), getServiceGroup(), clusters)

4. 修改application.yml配置

server:
  port: 8071

spring:
  application:
    name: order-seata-server
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848
    alibaba:
      seata:
        tx-service-group: my_test_tx_group # seata 服務事務分組(guangzhou)
seata:
  registry:
    # 配置seata注冊中心,告訴我們的seata client怎么去訪問我們的seata server(TC)
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848 # seata server 所在的nacos地址
      application: seata-server   # seata server 的服務名 默認就是seata-server
      username: nacos
      password: nacos
      group: SEATA_GROUP          # seata server 所在的組 默認就是SEATA_GROUP
  config:
    # 配置seata配置中心,告訴我們的seata client讀取我們的seata server(TC)的配置
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      username: nacos
      password: nacos
      group: SEATA_GROUP

5. 微服務發起者(TM 方)需要添加@GlobalTransactional注解

@Override
//@Transactional
@GlobalTransactional(name = "createOrder")
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. 測試

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM