在微服務(wù)開發(fā)過程中分布式事務(wù)一直是一個(gè)比較重要的問題,之前對于分布式事務(wù)的解決方法一般會通過MQ的最終一致性來解決,尤其是RocketMQ的事務(wù)消息,感興趣的可以看我的Spring Boot整合RocketMQ之事務(wù)消息。今天主要介紹一下是另一款阿里推出的分布式事務(wù)中間件——Seata。在我入職目前這家公司之前其實(shí)我對分布式事務(wù)的解決方案了解的也不多,也是在面試的時(shí)候當(dāng)時(shí)面試我的同事提出了Seata,而且目前公司有些部門已經(jīng)在生產(chǎn)環(huán)境上使用了。而我在的項(xiàng)目組是對老系統(tǒng)進(jìn)行重構(gòu),即從原有的單服務(wù)集群轉(zhuǎn)變?yōu)槲⒎?wù)架構(gòu),因此在分布式事務(wù)的解決方案上也選擇了Seata。疫情期間在家辦公的時(shí)候自己也對選擇的技術(shù)方案進(jìn)行了驗(yàn)證和測試,整體還是很不錯(cuò)的,而且我們項(xiàng)目也在生產(chǎn)中使用了Seata。
一、Seata簡介
這里我就不再過的介紹了,我還是推薦到官方去看相關(guān)的文檔,Seata官方。我的習(xí)慣一般不會做很多文字上的介紹,主要就是使用以及踩的坑,今天主要是通過Spring Boot整合Seata以及分布式事務(wù)的具體使用,下面直接開始準(zhǔn)備項(xiàng)目和其他準(zhǔn)備工作。
二、啟動(dòng)Seata的服務(wù)端
Seata是一個(gè)分布式事務(wù)中間件,使用它必須要啟動(dòng)服務(wù),然后微服務(wù)中的服務(wù),也就是Seata的客戶端會向Seata的服務(wù)端進(jìn)行注冊,注冊的時(shí)候會有攜帶該客戶端的一些相關(guān)信息,具體是什么后面我們會說到。
首先我們先去下載:官方下載地址,根據(jù)自己的需要選擇版本就行,目前我們項(xiàng)目組使用的是1.1.0的版本,但是我看最新的已經(jīng)到了1.3.0,所以這次我也下載1.3.0的最新版本。
下載之后解壓文件:
unzip seata-server-1.3.0.zip
這里說一下Seata服務(wù)端數(shù)據(jù)的存儲模式,目前是支持兩種一種是數(shù)據(jù)庫db,一種是文件file,默認(rèn)是使用的文件,但是個(gè)人覺得最好還是使用數(shù)據(jù)庫會好一點(diǎn)。Seata的全局事務(wù)會話信息由3部分內(nèi)容組成,全局事務(wù)、分支事務(wù)、全局鎖,它們對應(yīng)的表名分別為global_table、branch_table、lock_table。
下面我們創(chuàng)建Seata服務(wù)端需要的數(shù)據(jù)庫seata和表,建表腳本如下:
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(96),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
之后就是修改Seata的配置文件,打開解壓后的目錄,并進(jìn)入到conf目錄,這里主要是Seata的配置文件,分別是registry.conf和file.conf,這兩個(gè)文件主要是配置服務(wù)端配置的相關(guān)信息,因?yàn)槟J(rèn)使用的注冊類型是file(其他有nacos、eureka、redis等),即會從file.conf讀取相關(guān)配置。我們接下來就是修改file.conf,修改存儲模式為db,并配置db的相關(guān)信息,如下:
store {
## store mode: file、db、redis
mode = "db"
...
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.cj.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
}
....
}
配置完成之后我們一后臺啟動(dòng)的方式啟動(dòng)服務(wù)端,并將日志輸出到nohup.out:
nohup ./seata-server.sh -h 127.0.0.1 -p 8091 -m db &
三、項(xiàng)目準(zhǔn)備
測試分布式事務(wù)我這里就準(zhǔn)備兩個(gè)簡單的項(xiàng)目,一個(gè)訂單服務(wù)(order-service),一個(gè)倉庫服務(wù)(warehouse-service),這兩個(gè)項(xiàng)目分別使用不同的數(shù)據(jù)庫來模擬一個(gè)分布式事務(wù)。兩個(gè)項(xiàng)目的pom.xml中均添加Seata的依賴。服務(wù)之間的調(diào)用我就直接通過Feign來完成,不使用注冊中心。
...
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.13.RELEASE</version>
</parent>
...
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR5</spring-cloud.version>
<seata.version>1.3.0</seata.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--seata-->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.version}</version>
</dependency>
<!--postgresql-->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
...
為了能更好的測試分布式事務(wù),上述的倉庫服務(wù)和訂單服務(wù)使用了不同的數(shù)據(jù)庫平臺,其中:訂單服務(wù)使用Mysql,倉庫服務(wù)使用Postgresql,這點(diǎn)注意區(qū)分一下。兩個(gè)項(xiàng)目的配置文件如下:
訂單服務(wù)配置:
spring.application.name=order-service
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/order_db?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=123456
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.database=mysql
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.generate-ddl=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
## seata config
seata.enabled=true
seata.application-id=${spring.application.name}
seata.tx-service-group=${spring.application.name}-seata-service-group
seata.service.vgroup-mapping.order-service-seata-service-group=default
seata.service.enable-degrade=false
seata.service.disable-global-transaction=false
seata.service.grouplist.default=127.0.0.1:8091
倉庫服務(wù)配置:
server.port=18080
spring.application.name=warehouse-service
## db
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/warehouse_db?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
spring.datasource.username=postgres
spring.datasource.password=123456
## jpa
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.database=postgresql
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
spring.jpa.generate-ddl=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQL10Dialect
## seata config
seata.enabled=true
seata.application-id=${spring.application.name}
seata.tx-service-group=${spring.application.name}-seata-service-group
seata.service.vgroup-mapping.warehouse-service-seata-service-group=default
seata.service.enable-degrade=false
seata.service.disable-global-transaction=false
seata.service.grouplist.default=127.0.0.1:8091
上面創(chuàng)建的兩個(gè)項(xiàng)目我分別只寫了一個(gè)接口用于測試,訂單服務(wù)接口的Controller代碼如下:
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {
private OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping("/purchase")
public ResponseEntity purchase(@RequestBody OrderDTO orderDTO) {
Map<String,Object> resultMap = orderService.purchase(orderDTO);
return ResponseEntity.ok(resultMap);
}
}
Service代碼如下:
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private WarehouseClient warehouseClient;
@Autowired
private OrderRepository orderRepository;
@Override
@GlobalTransactional
public Map<String, Object> purchase(OrderDTO orderDTO) {
Map<String,Object> resultMap = new HashMap<>();
OrderEntity orderEntity = new OrderEntity();
orderEntity.setAddress(orderDTO.getAddress());
orderEntity.setCreateTime(new Date());
orderEntity.setTotalPrice(orderDTO.getTotalPrice());
orderEntity.setOrderNum(orderDTO.getOrderNum());
// 本地事務(wù)
OrderEntity result = orderRepository.save(orderEntity);
log.info(">>>> insert result = {} <<<<",result);
// 分支事務(wù)
Map<String,Object> response = warehouseClient.reduce(orderDTO.getWarehouseCode(),orderDTO.getNums());
log.info(">>>> warehouse response={}",response);
resultMap.put("success",true);
return resultMap;
}
}
通過上面的代碼,我們通過Feign客戶端調(diào)用倉庫服務(wù),當(dāng)然我這里并沒有開啟hystrix,所以異常信息會直接返回。上面代碼中主要需要關(guān)注的是@GlobalTransactional注解,這個(gè)是Seata提供的注解,主要就表明該事務(wù)是一個(gè)全局事務(wù),該注解還有一些可選參數(shù),這里就不介紹了。不管是訂單服務(wù)還是倉庫服務(wù)出現(xiàn)異常,整個(gè)涉及到的各個(gè)分支事務(wù)都會回滾。
倉庫服務(wù)的Service代碼如下:
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> reduceStock(String code, Long num) {
Map<String,Object> resultMap = new HashMap<>();
resultMap.put("success",false);
WarehouseEntity warehouseEntity = warehouseRepository.findByCode(code);
// 倉庫減少庫存
Long remain = warehouseEntity.getStock() - num;
warehouseEntity.setStock(remain);
warehouseEntity.setUpdateTime(new Date());
WarehouseEntity result = warehouseRepository.save(warehouseEntity);
// 模擬異常
if (result.getStock() % 2 == 0) {
log.error(">>>> 倉庫分支事務(wù)拋出異常 <<<<");
throw new RuntimeException("倉庫分支事務(wù)拋出異常");
}
resultMap.put("success",true);
resultMap.put("message","更新庫存成功");
return resultMap;
}
上面的代碼中和普通的本地事務(wù)沒有什么區(qū)別,通過判斷stock的數(shù)量是奇數(shù)還是偶數(shù)用來模擬異常情況。
注意:因?yàn)?code>Seata 通過代理數(shù)據(jù)源實(shí)現(xiàn)分支事務(wù),如果沒有進(jìn)行數(shù)據(jù)代理,事務(wù)無法成功回滾
關(guān)于實(shí)現(xiàn)數(shù)據(jù)源代理可以通過@EnableAutoDataSourceProxy實(shí)現(xiàn),也可以通過自定義代碼配置,比如:
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
// 或者使用其他數(shù)據(jù)源
return new DruidDataSource();
}
/**
* 將 代理數(shù)據(jù)源設(shè)置為主數(shù)據(jù)源
*/
@Primary
@Bean
public DataSource dataSource(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}
到這里項(xiàng)目準(zhǔn)備工作已經(jīng)基本完成了,現(xiàn)在還需要為沒一個(gè)服務(wù)創(chuàng)建一個(gè)表undo_log,這個(gè)表必須和該服務(wù)所在的表在同一個(gè)數(shù)據(jù)庫,Mysql的腳本如下,其他數(shù)據(jù)庫做相應(yīng)修改即可,
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=1 DEFAULT CHARSET=utf8mb4;
另外,單獨(dú)向倉庫服務(wù)的數(shù)據(jù)庫中插入兩條模擬數(shù)據(jù),到這里整個(gè)準(zhǔn)備階段的工作都算是完成了,接下就是進(jìn)行測試。
四、分布式事務(wù)測試
倉庫服務(wù)的初始信息如下:

接下來,分別啟動(dòng)訂單服務(wù)和倉庫服務(wù),并使用
Idea的Http Client創(chuàng)建相應(yīng)的HTTP請求:
POST http://localhost:8080/order/purchase
Accept: *
Content-Type: application/json
Cache-Control: no-cache
## 例子
{"totalPrice": 222.00,"orderNum": "222222","address": "CN-SC-CD-22","warehouseCode":"abc","nums": 3}
訂單服務(wù)的日志如下:
2020-09-20 19:58:32.141 INFO 28244 --- [nio-8080-exec-1] io.seata.tm.TransactionManagerHolder : TransactionManager Singleton io.seata.tm.DefaultTransactionManager@6597606d
2020-09-20 19:58:32.170 INFO 28244 --- [nio-8080-exec-1] i.seata.tm.api.DefaultGlobalTransaction : Begin new global transaction [172.17.0.1:8091:51035922649583616]
Hibernate: insert into t_order (address, createTime, orderNum, totalPrice, updateTime, warehouseCode) values (?, ?, ?, ?, ?, ?)
2020-09-20 19:58:32.401 INFO 28244 --- [nio-8080-exec-1] c.y.s.o.service.impl.OrderServiceImpl : >>>> insert result = OrderEntity(id=14, orderNum=222222, address=CN-SC-CD-22, totalPrice=222.0, warehouseCode=abc, createTime=Sun Sep 20 19:58:32 CST 2020, updateTime=null) <<<<
2020-09-20 19:58:32.677 INFO 28244 --- [nio-8080-exec-1] c.y.s.o.service.impl.OrderServiceImpl : >>>> warehouse response={success=true, message=更新庫存成功}
2020-09-20 19:58:32.692 INFO 28244 --- [nio-8080-exec-1] i.seata.tm.api.DefaultGlobalTransaction : [172.17.0.1:8091:51035922649583616] commit status: Committed
2020-09-20 19:58:33.246 INFO 28244 --- [ch_RMROLE_1_1_8] i.s.c.r.p.c.RmBranchCommitProcessor : rm client handle branch commit process:xid=172.17.0.1:8091:51035922649583616,branchId=51035923391975425,branchType=AT,resourceId=jdbc:mysql://localhost:3306/order_db,applicationData=null
2020-09-20 19:58:33.251 INFO 28244 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractRMHandler : Branch committing: 172.17.0.1:8091:51035922649583616 51035923391975425 jdbc:mysql://localhost:3306/order_db null
2020-09-20 19:58:33.254 INFO 28244 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractRMHandler : Branch commit result: PhaseTwo_Committed
根據(jù)日志可以看出,首先創(chuàng)建了一個(gè)全局事務(wù),該事務(wù)有一個(gè)全局172.17.0.1:8091:51035922649583616,另外可以看到訂單服務(wù)有一個(gè)分支事務(wù)branchId=51035923391975425,首先是全局事務(wù)提交成功,然后訂單分支事務(wù)開始提交,最后是PhaseTwo_Committed,即兩階段提交成功(我是猜的,并沒有看文檔,所以對準(zhǔn)確性不負(fù)責(zé))。另外也可以看出訂單分支事務(wù)的xid其實(shí)就是全局事務(wù)的id,且其類型是AT。
然后是倉庫服務(wù)的日志:
2020-09-20 19:58:32.488 INFO 28493 --- [io-18080-exec-1] c.y.s.w.controller.WarehouseController : >>>> request params: code=abc, nums=3 <<<<
2020-09-20 19:58:32.512 INFO 28493 --- [io-18080-exec-1] o.h.h.i.QueryTranslatorFactoryInitiator : HHH000397: Using ASTQueryTranslatorFactory
Hibernate: select warehousee0_.id as id1_0_, warehousee0_.code as code2_0_, warehousee0_.createTime as createTi3_0_, warehousee0_.stock as stock4_0_, warehousee0_.unit as unit5_0_, warehousee0_.updateTime as updateTi6_0_ from t_warehouse warehousee0_ where warehousee0_.code=?
Hibernate: update t_warehouse set code=?, createTime=?, stock=?, unit=?, updateTime=? where id=?
2020-09-20 19:58:56.095 INFO 28493 --- [eoutChecker_1_1] i.s.c.r.netty.NettyClientChannelManager : will connect to 127.0.0.1:8091
2020-09-20 19:58:56.096 INFO 28493 --- [eoutChecker_1_1] i.s.core.rpc.netty.NettyPoolableFactory : NettyPool create channel to transactionRole:TMROLE,address:127.0.0.1:8091,msg:< RegisterTMRequest{applicationId='warehouse-service', transactionServiceGroup='warehouse-service-seata-service-group'} >
2020-09-20 19:58:56.108 INFO 28493 --- [eoutChecker_1_1] i.s.c.rpc.netty.TmNettyRemotingClient : register TM success. client version:1.3.0, server version:1.3.0,channel:[id: 0x23f4f5db, L:/127.0.0.1:45528 - R:/127.0.0.1:8091]
2020-09-20 19:58:56.108 INFO 28493 --- [eoutChecker_1_1] i.s.core.rpc.netty.NettyPoolableFactory : register success, cost 7 ms, version:1.3.0,role:TMROLE,channel:[id: 0x23f4f5db, L:/127.0.0.1:45528 - R:/127.0.0.1:8091]
很可惜沒有看到有關(guān)全局事務(wù)和分支事務(wù)的日志輸出.....
這里我有點(diǎn)疑問,是不是我應(yīng)該創(chuàng)建三個(gè)服務(wù),請求從:服務(wù)A -> 服務(wù)B -> 服務(wù)C這樣是不是會更好些,這個(gè)有時(shí)間可以再測試一下。
我們看下兩個(gè)數(shù)據(jù)的數(shù)據(jù)情況:


和我們預(yù)期的結(jié)果一下,沒有問題,接下來測試下異常情況。
修改的HTTP請求:
POST http://localhost:8080/order/purchase
Accept: *
Content-Type: application/json
Cache-Control: no-cache
## 例子
{"totalPrice": 333.00,"orderNum": "333333","address": "CN-SC-CD-33","warehouseCode":"abc","nums": 7}
訂單服務(wù)的日志:
2020-09-20 20:16:40.815 INFO 28997 --- [Send_TMROLE_1_1] i.s.core.rpc.netty.NettyPoolableFactory : register success, cost 3 ms, version:1.3.0,role:TMROLE,channel:[id: 0xb4f2b43b, L:/127.0.0.1:45924 - R:/127.0.0.1:8091]
2020-09-20 20:16:40.825 INFO 28997 --- [nio-8080-exec-1] i.seata.tm.api.DefaultGlobalTransaction : Begin new global transaction [172.17.0.1:8091:51040488803799040]
Hibernate: insert into t_order (address, createTime, orderNum, totalPrice, updateTime, warehouseCode) values (?, ?, ?, ?, ?, ?)
2020-09-20 20:16:41.033 INFO 28997 --- [nio-8080-exec-1] c.y.s.o.service.impl.OrderServiceImpl : >>>> insert result = OrderEntity(id=15, orderNum=333333, address=CN-SC-CD-33, totalPrice=333.0, warehouseCode=abc, createTime=Sun Sep 20 20:16:40 CST 2020, updateTime=null) <<<<
2020-09-20 20:16:41.085 INFO 28997 --- [ch_RMROLE_1_1_8] i.s.c.r.p.c.RmBranchRollbackProcessor : rm handle branch rollback process:xid=172.17.0.1:8091:51040488803799040,branchId=51040489474887681,branchType=AT,resourceId=jdbc:mysql://localhost:3306/order_db,applicationData=null
2020-09-20 20:16:41.086 INFO 28997 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractRMHandler : Branch Rollbacking: 172.17.0.1:8091:51040488803799040 51040489474887681 jdbc:mysql://localhost:3306/order_db
2020-09-20 20:16:41.141 INFO 28997 --- [ch_RMROLE_1_1_8] i.s.r.d.undo.AbstractUndoLogManager : xid 172.17.0.1:8091:51040488803799040 branch 51040489474887681, undo_log deleted with GlobalFinished
2020-09-20 20:16:41.142 INFO 28997 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked
2020-09-20 20:16:41.165 INFO 28997 --- [nio-8080-exec-1] i.seata.tm.api.DefaultGlobalTransaction : [172.17.0.1:8091:51040488803799040] rollback status: Rollbacked
2020-09-20 20:16:41.196 ERROR 28997 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is feign.FeignException$InternalServerError: status 500 reading WarehouseClient#reduce(String,Long)] with root cause
首先依然是創(chuàng)建全局事務(wù):172.17.0.1:8091:51040488803799040,接著是訂單服務(wù)的分支事務(wù)branchId=51040489474887681回滾,且undo_log數(shù)據(jù)在全局事務(wù)完成也刪除了相關(guān)數(shù)據(jù)。Branch Rollbacked result: PhaseTwo_Rollbacked,分支事務(wù)回滾成功,最后是全局事務(wù)回滾成功,可以通過全局事務(wù)回滾狀態(tài)Rollbacked確定。
倉庫服務(wù)日志:
2020-09-20 20:16:41.043 INFO 28493 --- [io-18080-exec-3] c.y.s.w.controller.WarehouseController : >>>> request params: code=abc, nums=7 <<<<
Hibernate: select warehousee0_.id as id1_0_, warehousee0_.code as code2_0_, warehousee0_.createTime as createTi3_0_, warehousee0_.stock as stock4_0_, warehousee0_.unit as unit5_0_, warehousee0_.updateTime as updateTi6_0_ from t_warehouse warehousee0_ where warehousee0_.code=?
2020-09-20 20:16:41.046 ERROR 28493 --- [io-18080-exec-3] c.y.s.w.s.impl.WarehouseServiceImpl : >>>> 倉庫分支事務(wù)拋出異常 <<<<
很可惜依然沒有全局事務(wù)和分支事務(wù)的日志.....
我們再次確認(rèn)下數(shù)據(jù)庫:


連個(gè)表的數(shù)據(jù)和原來沒有任何的變化,也說明測試是成功的,倉庫服務(wù)發(fā)生異常時(shí)兩個(gè)服務(wù)的數(shù)據(jù)都成功回滾。測試還是很順利的,這些都只是
Seata客戶端的日志信息,接下來我們看下服務(wù)端的日志。
下面的日志請注意時(shí)間:
2020-09-20 19:58:32.161 INFO --- [LoggerPrint_1_1] i.s.c.r.p.server.BatchLogHandler : SeataMergeMessage timeout=60000,transactionName=purchase(com.ypc.seata.orderservice.entity.dto.OrderDTO)
,clientIp:127.0.0.1,vgroup:order-service-seata-service-group
2020-09-20 19:58:32.166 INFO --- [Thread_1_11_500] i.s.s.coordinator.DefaultCoordinator : Begin new global transaction applicationId: order-service,transactionServiceGroup: order-service-seata-service-group, transactionName: purchase(com.ypc.seata.orderservice.entity.dto.OrderDTO),timeout:60000,xid:172.17.0.1:8091:51035922649583616
2020-09-20 19:58:32.338 INFO --- [LoggerPrint_1_1] i.s.c.r.p.server.BatchLogHandler : SeataMergeMessage xid=172.17.0.1:8091:51035922649583616,branchType=AT,resourceId=jdbc:mysql://localhost:3306/order_db,lockKey=t_order:14
,clientIp:127.0.0.1,vgroup:order-service-seata-service-group
2020-09-20 19:58:32.347 INFO --- [Thread_1_12_500] i.seata.server.coordinator.AbstractCore : Register branch successfully, xid = 172.17.0.1:8091:51035922649583616, branchId = 51035923391975425, resourceId = jdbc:mysql://localhost:3306/order_db ,lockKeys = t_order:14
2020-09-20 19:58:32.679 INFO --- [LoggerPrint_1_1] i.s.c.r.p.server.BatchLogHandler : SeataMergeMessage xid=172.17.0.1:8091:51035922649583616,extraData=null
,clientIp:127.0.0.1,vgroup:order-service-seata-service-group
2020-09-20 19:58:33.275 INFO --- [cCommitting_1_1] io.seata.server.coordinator.DefaultCore : Committing global transaction is successfully done, xid = 172.17.0.1:8091:51035922649583616.
2020-09-20 19:58:37.619 INFO --- [NIOWorker_1_2_8] i.s.c.r.n.AbstractNettyRemotingServer : 127.0.0.1:45308 to server channel inactive.
2020-09-20 19:58:37.619 INFO --- [NIOWorker_1_2_8] i.s.c.r.n.AbstractNettyRemotingServer : remove channel:[id: 0xdc2f0baa, L:/127.0.0.1:8091 ! R:/127.0.0.1:45308]context:RpcContext{applicationId='order-service', transactionServiceGroup='order-service-seata-service-group', clientId='order-service:127.0.0.1:45308', channel=[id: 0xdc2f0baa, L:/127.0.0.1:8091 ! R:/127.0.0.1:45308], resourceSets=null}
2020-09-20 19:58:38.002 INFO --- [NIOWorker_1_1_8] i.s.c.r.n.AbstractNettyRemotingServer : 127.0.0.1:45252 to server channel inactive.
2020-09-20 19:58:38.003 INFO --- [NIOWorker_1_1_8] i.s.c.r.n.AbstractNettyRemotingServer : remove channel:[id: 0x584d000b, L:/127.0.0.1:8091 ! R:/127.0.0.1:45252]context:RpcContext{applicationId='order-service', transactionServiceGroup='order-service-seata-service-group', clientId='order-service:127.0.0.1:45252', channel=[id: 0x584d000b, L:/127.0.0.1:8091 ! R:/127.0.0.1:45252], resourceSets=[]}
2020-09-20 19:58:56.105 INFO --- [NIOWorker_1_4_8] i.s.c.r.processor.server.RegTmProcessor : TM register success,message:RegisterTMRequest{applicationId='warehouse-service', transactionServiceGroup='warehouse-service-seata-service-group'},channel:[id: 0x5d296a48, L:/127.0.0.1:8091 - R:/127.0.0.1:45528],client version:1.3.0
上面內(nèi)容是是第一次正常訪問時(shí)開啟全局事務(wù)時(shí)服務(wù)端日志輸出,時(shí)間也完全符合,且全局的事務(wù)id也是一致的。
下面是第二次異常情況的日志輸出:
2020-09-20 20:16:40.817 INFO --- [LoggerPrint_1_1] i.s.c.r.p.server.BatchLogHandler : SeataMergeMessage timeout=60000,transactionName=purchase(com.ypc.seata.orderservice.entity.dto.OrderDTO)
,clientIp:127.0.0.1,vgroup:order-service-seata-service-group
2020-09-20 20:16:40.822 INFO --- [Thread_1_16_500] i.s.s.coordinator.DefaultCoordinator : Begin new global transaction applicationId: order-service,transactionServiceGroup: order-service-seata-service-group, transactionName: purchase(com.ypc.seata.orderservice.entity.dto.OrderDTO),timeout:60000,xid:172.17.0.1:8091:51040488803799040
2020-09-20 20:16:40.976 INFO --- [LoggerPrint_1_1] i.s.c.r.p.server.BatchLogHandler : SeataMergeMessage xid=172.17.0.1:8091:51040488803799040,branchType=AT,resourceId=jdbc:mysql://localhost:3306/order_db,lockKey=t_order:15
,clientIp:127.0.0.1,vgroup:order-service-seata-service-group
2020-09-20 20:16:40.984 INFO --- [Thread_1_17_500] i.seata.server.coordinator.AbstractCore : Register branch successfully, xid = 172.17.0.1:8091:51040488803799040, branchId = 51040489474887681, resourceId = jdbc:mysql://localhost:3306/order_db ,lockKeys = t_order:15
2020-09-20 20:16:41.077 INFO --- [LoggerPrint_1_1] i.s.c.r.p.server.BatchLogHandler : SeataMergeMessage xid=172.17.0.1:8091:51040488803799040,extraData=null
,clientIp:127.0.0.1,vgroup:order-service-seata-service-group
2020-09-20 20:16:41.150 INFO --- [Thread_1_19_500] io.seata.server.coordinator.DefaultCore : Rollback branch transaction successfully, xid = 172.17.0.1:8091:51040488803799040 branchId = 51040489474887681
2020-09-20 20:16:41.158 INFO --- [Thread_1_19_500] io.seata.server.coordinator.DefaultCore : Rollback global transaction successfully, xid = 172.17.0.1:8091:51040488803799040.
2020-09-20 20:17:11.856 INFO --- [NIOWorker_1_6_8] i.s.c.r.n.AbstractNettyRemotingServer : 127.0.0.1:45924 to server channel inactive.
2020-09-20 20:17:11.856 INFO --- [NIOWorker_1_6_8] i.s.c.r.n.AbstractNettyRemotingServer : remove channel:[id: 0xa0be40ed, L:/127.0.0.1:8091 ! R:/127.0.0.1:45924]context:RpcContext{applicationId='order-service', transactionServiceGroup='order-service-seata-service-group', clientId='order-service:127.0.0.1:45924', channel=[id: 0xa0be40ed, L:/127.0.0.1:8091 ! R:/127.0.0.1:45924], resourceSets=null}
2020-09-20 20:17:12.206 INFO --- [NIOWorker_1_5_8] i.s.c.r.n.AbstractNettyRemotingServer : 127.0.0.1:45872 to server channel inactive.
2020-09-20 20:17:12.207 INFO --- [NIOWorker_1_5_8] i.s.c.r.n.AbstractNettyRemotingServer : remove channel:[id: 0xca7c6e7d, L:/127.0.0.1:8091 ! R:/127.0.0.1:45872]context:RpcContext{applicationId='order-service', transactionServiceGroup='order-service-seata-service-group', clientId='order-service:127.0.0.1:45872', channel=[id: 0xca7c6e7d, L:/127.0.0.1:8091 ! R:/127.0.0.1:45872], resourceSets=[]}
服務(wù)端的日志也沒有看到倉庫服務(wù)的情況,但是啟動(dòng)時(shí)確實(shí)有看到注冊成功,但是事務(wù)提交和回滾都只看到訂單服務(wù)的相關(guān)信息,這個(gè)有點(diǎn)不太懂了,也許應(yīng)該再增加一個(gè)服務(wù)試試。
五、總結(jié)
本次關(guān)于Spring Boot整合Seata以及測試分布式事務(wù)的學(xué)習(xí)就到這里,代碼我已經(jīng)提交到我的github了。當(dāng)然在項(xiàng)目中我直接引入的是starter依賴,感興趣的話其實(shí)可以試試直接引入seata-all依賴,我之前項(xiàng)目組使用的就是這種方式,相比使用starter要稍微麻煩一點(diǎn),不過從中也可以更好的理解相關(guān)的配置。其實(shí)我個(gè)人不太喜歡也不擅長直接從原理開始,所以個(gè)人一般都是直接從項(xiàng)目開,主要學(xué)習(xí)怎么使用,后面慢慢的可以了解下原理甚至是源碼。最后希望大家能關(guān)注下我的個(gè)人號:超超學(xué)堂,也歡迎大家多多交流討論。