分布式事務(wù)解決方案——Seata使用

在微服務(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.conffile.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ù)的初始信息如下:

圖-1.png

接下來,分別啟動(dòng)訂單服務(wù)和倉庫服務(wù),并使用IdeaHttp 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ù)情況:

倉庫服務(wù)數(shù)據(jù)圖-1.png

訂單服務(wù)數(shù)據(jù)圖-1.png

和我們預(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ù)庫:

倉庫服務(wù)數(shù)據(jù)圖-2.png

訂單服務(wù)數(shù)據(jù)圖-2.png

連個(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é)堂,也歡迎大家多多交流討論。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容