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 中,一個分布式事務的生命周期如下:
- TM 請求 TC 開啟一個全局事務。TC 會生成一個 XID 作為該全局事務的編號。XID,會在微服務的調用鏈路中傳播,保證將多個微服務的子事務關聯在一起。
當一進入事務方法中就會生成XID , global_table 就是存儲的全局事務信息 , - RM 請求 TC 將本地事務注冊為全局事務的分支事務,通過全局事務的 XID 進行關聯。
當運行數據庫操作方法,branch_table 存儲事務參與者 - TM 請求 TC 告訴 XID 對應的全局事務是進行提交還是回滾。
- 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共享,相應性能差些
-
- 打開config/file.conf
- 修改mode="db"
- 修改數據庫連接信息(URL\USERNAME\PASSWORD)
- 創建數據庫seata_server
- 新建表: 可以去seata提供的資源信息中下載:https://github.com/seata/seata/tree/1.3.0/script
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. 測試
分布式事務成功,模擬正常下單、扣庫存,扣余額;
分布式事務失敗,模擬下單扣庫存成功、扣余額失敗,事務是否回滾;