1. 程式人生 > 其它 >Alibaba微服務元件 - Seata(二) 分散式事務Seata使用

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 中,一個分散式事務的生命週期如下:


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

2.1.2 設計思路

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


業務資料和回滾日誌記錄在同一個本地事務中提交,釋放本地鎖和連線資源。核心在於對業務sql進行解析,轉換成undolog,並同時入庫,這是怎麼做的呢?先丟擲一個概念DataSourceProxy代理資料來源,通過名字大家大概也能基本猜到是什麼個操作,後面做具體分析。
參考官方文件: https://seata.io/zh-cn/docs/dev/mode/at-mode.html

第二階段:
分散式事務操作成功,則TC通知RM非同步刪除undolog

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

整體執行流程

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配置

然後啟動註冊中心Nacos Server

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

配置Nacos配置中心

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

獲取/seata/script/config-center/config.txt,修改配置資訊

配置事務分組, 要與客戶端配置的事務分組一致
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"

配置引數同步到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欄位, 預設值為空 ''

精簡配置

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

配置寫入成功:

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

叢集啟動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

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

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"
  }
}

如果出現這種問題:

一般大多數情況下都是因為配置不匹配導致的:

  • 檢查現在使用的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. 測試

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