分佈式事務 Seata 業務系統集成
-
業務系統集成 - Client端(TM,RM) ,使用 seata 0.9 實現分佈式事務
-
先參考seata-samples 項目,完成一個 dubbo項目,如:my demo
-
創建storage, account, order 三個服務提供者,和一個 business 消費者
-
下圖是添加 seata 分佈式事務後的處理流程
-
-
添加pom依賴
<dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>1.1.0</version> </dependency> <!--1. dubbo 依賴請參見上面 Spring boot 2.1.9 + Dubbo 2.7.3 + Nacos 1.1.4 --> <!--2. druid 依賴和 mybatis 依賴請參見 MySQL.md-->
-
建表: undo_log ,SQL語句在源碼的script --> client -->at–> db目錄下
-- 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` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id', `branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(100) 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 NOT NULL COMMENT 'create datetime', `log_modified` DATETIME NOT NULL COMMENT 'modify datetime', PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
-
配置參數:將file.conf和registry.conf複製到 spring boot工程resource 目錄下
-
file.conf和registry.conf 文件位置在源碼的script --> client -->config 目錄下
-
如果以file形式註冊,默認配置不用改
-
但是file.conf有一處要根據服務名來修改
vgroupMapping.xxx_tx_group = "default" # xxx表示你的spring.application.name
-
問題:如果忘記複製file.conf和registry.conf 文件,會報錯: NotSupportYetException: not support register type: null
-
注意:pom 依賴 seata-all , 不能改成 seata-spring-boot-starter ,否則無法識別 file.conf和registry.conf 文件
-
-
配置數據源代理,支持自動和手動兩種配置。
// 開啓自動配置:使用註解 @EnableAutoDataSourceProxy 開啓數據源代理 // 手動配置:需要將業務數據源放到代理數據源中,另外mybatis的SqlSessionFactory也需要手動配置,將代理數據源寫入mybatis的SqlSessionFactory中 @Bean("dataSource") public DataSourceProxy dataSource(DataSource druidDataSource) { return new DataSourceProxy(druidDataSource); } // 將代理數據源寫入mybatis的SqlSessionFactory中 @Bean public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSourceProxy); factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath*:/mappers/*.xml")); factoryBean.setTransactionFactory(new JdbcTransactionFactory()); return factoryBean.getObject(); }
-
初始化 GlobalTransactionScanner - 全局事務掃描器 , 也支持自動和手動兩種
- GlobalTransactionScanner負責執行自動掃描註解包含GlobalTransactional註解的代碼邏輯
- GlobalTransactionalInterceptor負責對包含GlobalTransactional註解進行攔截
// 自動配置: 引入seata-spring-boot-starter 即可 // 手動配置 public GlobalTransactionScanner globalTransactionScanner() { String applicationName = this.applicationContext.getEnvironment() .getProperty("spring.application.name"); String txServiceGroup = this.seataProperties.getTxServiceGroup(); if (StringUtils.isEmpty(txServiceGroup)) { txServiceGroup = applicationName + "-fescar-service-group"; this.seataProperties.setTxServiceGroup(txServiceGroup); } return new GlobalTransactionScanner(applicationName, txServiceGroup); }
-
實現xid跨服務傳遞 , 似乎 1.1.0 版本內部已經實現了xid傳遞
- Seata上的註解@Activate(group = {Constants.PROVIDER, Constants.CONSUMER}, order = 100),表示dubbo的服務提供方跟消費方都會觸發到這個過濾器,所以我們的Seata發起者會產生一個XID的傳遞
-
在需要分佈式事務的方法上添加@GlobalTransactional註解,就這麼簡單。
import org.springframework.stereotype.Service; @Service public class BusinessServiceImp implements BusinessService { @Reference(version = "0.0.1") private StorageService storageService; @Reference(version = "0.0.1") private OrderService orderService; //通過@GlobalTransactional註解的方法會走攔截器GlobalTransactionalInterceptor的執行邏輯 @GlobalTransactional(timeoutMills = 300000, name = "dubbo-gts-seata-example") @Override public ObjectResponse handleBusiness(BusinessDTO businessDTO) { System.out.println("開始全局事務,XID = " + RootContext.getXID()); ObjectResponse<Object> objectResponse = new ObjectResponse<>(); //1、扣減庫存 ObjectResponse response1 = storageService.decreaseStorage(commodityDTO); //2、創建訂單 ObjectResponse<OrderDTO> response2 = orderService.createOrder(orderDTO); return objectResponse; } }
-
爲什麼只要添加一個 @GlobalTransactional註解 就可以了呢?請看RM原理
-
Phase 1 ( 1階段)
- 當前本地事務是否處於全局事務中(也就判斷
ConnectionContext
中的 xid 是否爲空) - 如果不處於全局事務中,但是有全局事務鎖(即方法標註了
@GlobalLock
註解),則在全局事務鎖中提交本地事務。 - 以上情況都不是,則調用
targetConnection
對本地事務進行 commit。 - 如果處於全局事務中,首先創建分支事務,再將
ConnectionContext
中的 UndoLog 寫入到undo_log
表中,然後調用targetConnection
對本地事務進行 commit,將 UndoLog 與業務 SQL 一起提交,最後上報分支事務的狀態(成功 or 失敗),並將ConnectionContext
上下文重置。
- 當前本地事務是否處於全局事務中(也就判斷
-
Phase 2 (2階段)
- 提交
- 回滾
-
-
-
在項目使用 seata-spring-boot-starter 依賴
-
添加 pom 依賴
<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.1.0</version> </dependency>
-
刪除 file.conf和registry.conf 文件
-
刪除 手動配置數據源代理 和 手動 初始化的 GlobalTransactionScanner 相關代碼,這些都自動完成了
-
在 properties 文件中添加 seata 配置 , spring 相關配置在源碼的script --> client -->spring 目錄下
#================seata config======================= seata.enabled=true seata.application-id=dubbo-account-example seata.tx-service-group=my_test_tx_group seata.enable-auto-data-source-proxy=true seata.use-jdk-proxy=false # ----- begin: 不在源碼的默認配置裏面 , 屬於 client和Server的公共配置--------- # 使用 nacos 註冊類型,通過nacos 發現 seata-server seata.registry.type=nacos seata.registry.nacos.server-addr=112.74.179.118:8848 seata.registry.nacos.cluster=default seata.registry.nacos.namespace=9a6af877-1cd4-4450-9eff-7ba6252fca8e # 使用 nacos 配置中心 seata.config.type=nacos seata.config.nacos.server-addr=112.74.179.118:8848 seata.config.nacos.namespace=9a6af877-1cd4-4450-9eff-7ba6252fca8e seata.config.nacos.group=SEATA_GROUP # ----- end: 不在源碼的默認配置裏面 --------- seata.service.vgroup-mapping.my_test_tx_group=default seata.service.enable-degrade=false seata.service.disable-global-transaction=false # 只會在 register.type = file 時,纔會被使用,其他類型不會讀取 seata.service.grouplist.default=127.0.0.1:6091 seata.client.rm.async-commit-buffer-limit=1000 seata.client.rm.report-retry-count=5 seata.client.rm.table-meta-check-enable=false seata.client.rm.report-success-enable=false seata.client.rm.lock.retry-interval=10 seata.client.rm.lock.retry-times=30 seata.client.rm.lock.retry-policy-branch-rollback-on-conflict=true seata.client.tm.commit-retry-count=5 seata.client.tm.rollback-retry-count=5 seata.client.undo.data-validation=true seata.client.undo.log-serialization=jackson seata.client.undo.log-table=undo_log seata.client.log.exceptionRate=100 seata.transport.type=TCP seata.transport.server=NIO seata.transport.heartbeat=true seata.transport.serialization=seata seata.transport.compressor=none seata.transport.enable-client-batch-send-request=true seata.transport.shutdown.wait=3 seata.transport.thread-factory.boss-thread-prefix=NettyBoss seata.transport.thread-factory.worker-thread-prefix=NettyServerNIOWorker seata.transport.thread-factory.server-executor-thread-prefix=NettyServerBizHandler seata.transport.thread-factory.share-boss-worker=false seata.transport.thread-factory.client-selector-thread-prefix=NettyClientSelector seata.transport.thread-factory.client-selector-thread-size=1 seata.transport.thread-factory.client-worker-thread-prefix=NettyClientWorkerThread seata.transport.thread-factory.worker-thread-size=default seata.transport.thread-factory.boss-thread-size=1
-
注意: 如果要修改 my_test_tx_group ,需要修改兩個地方,否則會報錯:
no available service 'null' found, please make sure registry config correct
seata.tx-service-group=my_test_tx_group seata.service.vgroup-mapping.my_test_tx_group=default
-
上面關於 seata的配置,也可以放到 nacos 配置中心
-
在nacos 配置中新建一個 nacos-seata-config.properties 配置文件,將上面的那麼多配置copy進來
-
啓用 配置文件
@SpringBootApplication // 自動加載配置資源 @NacosPropertySource(dataId = "dubbo-demo-api", autoRefreshed = true) @NacosPropertySource(dataId = "nacos-seata-config.properties", autoRefreshed = true) public class NacosConfigApplication { public static void main(String[] args) { SpringApplication.run(NacosConfigApplication.class, args); } }
-
-
-
seata 參數配置介紹
-
關注的配置, 具體說明參見下面
server端 client端 registry.type registry.type config.type config.type store.mode service.vgroupMapping.my_test_tx_group store.db.driverClassName service.default.grouplist store.db.url service.disableGlobalTransaction store.db.user store.db.password -
公共部分
key desc remark transport.serialization client和server通信編解碼方式 seata(ByteBuf)、protobuf、kryo、hession,默認seata transport.compressor client和server通信數據壓縮方式 none、gzip,默認none transport.heartbeat client和server通信心跳檢測開關 默認true開啓 registry.type 註冊中心類型 默認file,支持file 、nacos 、eureka、redis、zk、consul、etcd3、sofa、custom config.type 配置中心類型 默認file,支持file、nacos 、apollo、zk、consul、etcd3、custom -
Server 端
- store.mode : 事務會話信息存儲方式, file本地文件(不支持HA),db數據庫(支持HA)
- store.db.driverClassName: db模式數據庫驅動
- store.db.url : db模式數據庫url , 默認jdbc:mysql://127.0.0.1:3306/seata
-
Client 端
- service.vgroupMapping.my_test_tx_group : my_test_tx_group爲分組,配置項值爲TC集羣名
- service.default.grouplist : 僅註冊中心爲file時使用
- service.disableGlobalTransaction : 全局事務開關,默認false。false爲開啓,true爲關閉
-
事務分組說明
1.事務分組是什麼?
事務分組是seata的資源邏輯,類似於服務實例。在file.conf中的my_test_tx_group就是一個事務分組。
2.通過事務分組如何找到後端集羣?
首先程序中配置了事務分組(GlobalTransactionScanner 構造方法的txServiceGroup參數),程序會通過用戶配置的配置中心去尋找service.vgroupMapping .事務分組配置項,取得配置項的值就是TC集羣的名稱。拿到集羣名稱程序通過一定的前後綴+集羣名稱去構造服務名,各配置中心的服務名實現不同。拿到服務名去相應的註冊中心去拉取相應服務名的服務列表,獲得後端真實的TC服務列表。
3.爲什麼這麼設計,不直接取服務名?
這裏多了一層獲取事務分組到映射集羣的配置。這樣設計後,事務分組可以作爲資源的邏輯隔離單位,當發生故障時可以快速failover。
-
grouplist 說明
1.什麼時候會用到file.conf中的default.grouplist?
當registry.type=file時會用到,其他時候不讀。
2.default.grouplist的值列表是否可以配置多個?
可以配置多個,配置多個意味着集羣,但當store.mode=file時,會報錯。原因是在file存儲模式下未提供本地文件的同步,所以需要使用store.mode=db,通過db來共享TC集羣間數據
3.是否推薦使用default.grouplist?
不推薦,如問題1,當registry.type=file時會用到,也就是說這裏用的不是真正的註冊中心,不具體服務的健康檢查機制當tc不可用時無法自動剔除列表,推薦使用nacos 、eureka、redis、zk、consul、etcd3、sofa。registry.type=file或config.type=file 設計的初衷是讓用戶再不依賴第三方註冊中心或配置中心的前提下,通過直連的方式,快速驗證seata服務。
4.seata-spring-boot-starter中的配置爲什麼是grouplist.default,也就是說和file.conf中的default.grouplist寫法剛好顛倒了位置?
由於spring-boot本身配置文件語法的要求,這個地方需要將file.conf中的default.grouplist寫成grouplist.default,效果是一樣的.
-
-
相關測試和常見問題
-
測試時發現 undo_log 表中 有很多 log_status=1的記錄,這是做什麼用的?
- 場景 : 分支事務a註冊TC後,a的本地事務提交前發生了全局事務回滾
- 說明: log_status=1的是防禦性的,是收到全局回滾請求,但是不確定某個事務分支的本地事務是否已經執行完成了,這時事先插入一條branchid相同的數據,插入的假數據成功了,本地事務繼續執行就會報主鍵衝突自動回滾。 假如插入不成功說明表裏有數據這個本地事務已經執行完成了,那麼取出這條undolog數據做反向回滾操作。
- 結果 : 全局事務回滾成功後,確保 a資源不會Commit成功,因此是正常現象。
-
併發測試 http://localhost:9000/business/buy 接口,可能會出現如下異常
curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{ "amount": 20, "commodityCode": "C201901140001", "count": 2, "name": "水杯", "userId": "1" }' 'http://localhost:9000/business/buy'
// StorageService 報的異常 2020-02-25 13:27:48.036 ERROR 33029 --- [20883-thread-36] .d.r.f.ExceptionFilter$ExceptionListener : [DUBBO] Got unchecked and undeclared exception which called by 192.168.1.6. service: com.example.common.service.StorageService, method: decreaseStorage, exception: org.springframework.jdbc.UncategorizedSQLException: ### Error updating database. Cause: java.sql.SQLException: .... ### SQL: update t_storage set count = count-1 where commodity_code = ? ... Caused by: java.sql.SQLException: io.seata.core.exception.RmTransactionException: Response[ TransactionException[Could not found global transaction xid = 192.168.1.6:6091:2036330807] ] at io.seata.rm.datasource.ConnectionProxy.recognizeLockKeyConflictException(ConnectionProxy.java:153) ~[seata-all-1.1.0.jar:1.1.0] at io.seata.rm.datasource.ConnectionProxy.processGlobalTransactionCommit(ConnectionProxy.java:215) ~[seata-all-1.1.0.jar:1.1.0] at io.seata.rm.datasource.ConnectionProxy.doCommit(ConnectionProxy.java:192) ~[seata-all-1.1.0.jar:1.1.0] at io.seata.rm.datasource.ConnectionProxy$LockRetryPolicy.execute(ConnectionProxy.java:283) ~[seata-all-1.1.0.jar:1.1.0] ... at io.seata.rm.datasource.ConnectionProxy$LockRetryPolicy.doRetryOnLockConflict(ConnectionProxy.java:293) ~[seata-all-1.1.0.jar:1.1.0] ... at io.seata.rm.datasource.PreparedStatementProxy.execute(PreparedStatementProxy.java:54) ~[seata-all-1.1.0.jar:1.1.0] at org.apache.ibatis.executor.statement.PreparedStatementHandler.update(PreparedStatementHandler.java:47) ~[mybatis-3.5.3.jar:3.5.3] ... org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:426) ~[mybatis-spring-2.0.3.jar:2.0.3] ... 52 common frames omitted
// seata server 報的異常 2020-02-25 13:27:46.229 ERROR[ServerHandlerThread_1_500]io.seata.core.exception.AbstractExceptionHandler.exceptionHandleTemplate:120 -Catch TransactionException while do RPC, request: xid=192.168.1.6:6091:2036330807,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/test,lockKey=t_storage:1 io.seata.core.exception.BranchTransactionException: Global lock acquire failed xid = 192.168.1.6:6091:2036330807 branchId = 2036330925 at io.seata.server.transaction.at.ATCore.branchSessionLock(ATCore.java:48) at io.seata.server.coordinator.AbstractCore.lambda$branchRegister$0(AbstractCore.java:77) at io.seata.server.session.GlobalSession.lockAndExecute(GlobalSession.java:616) at io.seata.server.coordinator.AbstractCore.branchRegister(AbstractCore.java:72) at io.seata.server.coordinator.DefaultCore.branchRegister(DefaultCore.java:95) ... 2020-02-25 13:27:46.864 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.AbstractCore.lambda$branchRegister$0:86 -Successfully register branch xid = 192.168.1.*:6091:2036330816, branchId = 2036330927 2020-02-25 13:27:47.014 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.DefaultCore.doGlobalRollback:334 -Successfully rollback global, xid = 192.168.1.*:6091:2036330807 2020-02-25 13:27:47.068 ERROR[ServerHandlerThread_1_500]io.seata.core.exception.AbstractExceptionHandler.exceptionHandleTemplate:120 -Catch TransactionException while do RPC, request: xid=192.168.1.*:6091:2036330807,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/test,lockKey=t_storage:1 io.seata.core.exception.GlobalTransactionException: Could not found global transaction xid = 192.168.1.*:6091:2036330807 at ...
-
測試 http://localhost:9000/business/buy 和 http://localhost:8001/account/test_global_lock 接口
curl -X GET http://localhost:8001/account/test_global_lock
全局事務,XID = 192.168.1.*:6091:2036330432 2020-02-25 12:58:29.798 INFO 34142 --- [nio-8001-exec-7] c.e.a.controller.AccountController : testGlobalLock Hi, i got lock, i will do some thing with holding this lock. 2020-02-25 12:58:29.883 ERROR 34142 --- [nio-8001-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.transaction.TransactionSystemException: Could not commit JDBC transaction; nested exception is io.seata.rm.datasource.exec.LockConflictException] with root cause io.seata.rm.datasource.exec.LockConflictException: null at io.seata.rm.datasource.ConnectionProxy.checkLock(ConnectionProxy.java:116) ~[seata-all-1.1.0.jar:1.1.0] ... at io.seata.rm.datasource.ConnectionProxy$LockRetryPolicy.execute(ConnectionProxy.java:283) ~[seata-all-1.1.0.jar:1.1.0] at io.seata.rm.datasource.ConnectionProxy.commit(ConnectionProxy.java:179) ~[seata-all-1.1.0.jar:1.1.0] ... at io.seata.spring.annotation.GlobalTransactionalInterceptor.handleGlobalLock(GlobalTransactionalInterceptor.java:92) ~[seata-all-1.1.0.jar:1.1.0] ... at com.sun.proxy.$Proxy85.testGlobalLock(Unknown Source) ~[na:na] at com.example.account.controller.AccountController.testGlobalLock(AccountController.java:23) ~[classes/:na] at 2020-02-25 12:58:30.898 INFO 34142 --- [atch_RMROLE_1_8] i.s.core.rpc.netty.RmMessageListener : onMessage:xid=192.168.1.6:6091:2036330432,branchId=2036330436,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/test,applicationData=null 2020-02-25 12:58:30.916 INFO 34142 --- [atch_RMROLE_1_8] io.seata.rm.AbstractRMHandler : Branch committing: 192.168.1.6:6091:2036330432 2036330436 jdbc:mysql://127.0.0.1:3306/test null 2020-02-25 12:58:30.917 INFO 34142 --- [atch_RMROLE_1_8] io.seata.rm.AbstractRMHandler : Branch commit result: PhaseTwo_Committed
-
-
每次執行insert 、 update 數據庫時,RM 都會拋出異常
Unable to commit against JDBC Connection
-
網上搜索到類似的問題,說是:“一開始都很正常,可在一次事務更新的條數增多後,開始出現異常”
- 雖然現象和我們的不大一樣,但也得到了啓發,查看 seata-server 端,是否也拋出異常
-
查看seata-server日誌,發現真正原因是:Caused by: io.seata.common.exception.StoreException: Data truncation: Data too long for column ‘row_key’ at row 1
- 意思是:數據太長了,超出 row_key 列的範圍
-
解決方法:將 seata 庫的 lock_table 表的主鍵 row_key 字段的長度從128改成250後,問題就解決了
-
搜索這個問題時,發現作者已經修復這個bug : bugfix: AT mode resourceId(row_key) too long ,但是爲什麼還拋出異常呢?
-
原因分析:
-
根據修復bug的線索,得知 row_key 字段保存的數據有一部分是 jdbcUrl
-
然後繼續查找 seata-server 的源碼發現 row_key 字段保存的數據爲
// rowkey 由 jdbcUrl(數據庫連接字符串,resourceId) + 表名 + 主鍵 3部分組成 lockDO.setRowKey(getRowKey(rowLock.getResourceId(), rowLock.getTableName(), rowLock.getPk())); // 中間用 "^^^" 分割 protected String getRowKey(String resourceId, String tableName, String pk) { return new StringBuilder().append(resourceId).append(LOCK_SPLIT).append(tableName).append(LOCK_SPLIT).append(pk) .toString(); } LOCK_SPLIT = "^^^";
-
由於項目使用的是阿里雲數據庫, jdbcUrl 會比較長(70個字符),加上表的主鍵使用的是UUID也比較長(38個字符),這樣留給 表名的長度就很少了(128-70-38=14),也就是說表名不能超過14個字符,如果超過,就會拋出異常:Data too long for column ‘row_key’ at row 1
-
-