微服務痛點-基於Dubbo + Seata的分散式事務(AT)模式

三升水發表於2020-12-22

前言

Seata 是一款開源的分散式事務解決方案,致力於提供高效能和簡單易用的分散式事務服務。Seata 將為使用者提供了 AT、TCC、SAGA 和 XA 事務模式,為使用者打造一站式的分散式解決方案。對於Seata不太瞭解的朋友,可以看下我之前寫的文章: 微服務開發的最大痛點-分散式事務SEATA入門簡介

AT模式

AT模式怎麼理解

AT模式下,每個資料庫被當做是一個Resource,Seata 裡稱為 DataSource Resource。業務通過 JDBC 標準介面訪問資料庫資源時,Seata 框架會對所有請求進行攔截,做一些操作。

每個本地事務提交時,Seata RM(Resource Manager,資源管理器) 都會向 TC(Transaction Coordinator,事務協調器) 註冊一個分支事務。當請求鏈路呼叫完成後,發起方通知 TC 提交或回滾分散式事務,進入二階段呼叫流程。此時,TC 會根據之前註冊的分支事務回撥到對應參與者去執行對應資源的第二階段。

TC 是怎麼找到分支事務與資源的對應關係呢?每個資源都有一個全域性唯一的資源 ID,並且在初始化時用該 ID 向 TC 註冊資源。在執行時,每個分支事務的註冊都會帶上其資源 ID。這樣 TC 就能在二階段呼叫時正確找到對應的資源。這就是我們的 AT 模式。簡單總結一下,就是把每個資料庫當做一個 Resource,在本地事務提交時會去註冊一個分支事務。

AT模式是一種無侵入的分散式事務解決方案。在AT模式下,使用者只需關注自己的"業務SQL",使用者的"業務SQL"作為第一階段,Seata框架會自動生成事務的二階段提交和回滾操作。

AT模式如何做到對業務的無侵入

  • 一階段:

在一階段,Seata 會攔截“業務 SQL”,首先解析 SQL 語義,找到“業務 SQL”要更新的業務資料,在業務資料被更新前,將其儲存成“before image”,然後執行“業務 SQL”更新業務資料,在業務資料更新之後,再將其儲存成“after image”,最後生成行鎖。以上操作全部在一個資料庫事務內完成,這樣保證了一階段操作的原子性。

  • 二階段提交:

二階段如果是提交的話,因為“業務 SQL”在一階段已經提交至資料庫, 所以 Seata 框架只需將一階段儲存的快照資料和行鎖刪掉,完成資料清理即可。

  • 二階段回滾:

二階段如果是回滾的話,Seata 就需要回滾一階段已經執行的“業務 SQL”,還原業務資料。回滾方式便是用“before image”還原業務資料;但在還原前要首先要校驗髒寫,對比“資料庫當前業務資料”和 “after image”,如果兩份資料完全一致就說明沒有髒寫,可以還原業務資料,如果不一致就說明有髒寫,出現髒寫就需要轉人工處理。

AT 模式的一階段、二階段提交和回滾均由 Seata 框架自動生成,使用者只需編寫“業務 SQL”,便能輕鬆接入分散式事務,AT 模式是一種對業務無任何侵入的分散式事務解決方案。

當然官網對AT模式也進行了細緻的講解, 大家可以看下Seata官網的Seata AT模式

Dubbo + Seata 實戰案例

環境準備

Dubbo

docker-compose.yaml:

version: '3'

services:
  zookeeper:
    image: zookeeper
    ports:
      - 2181:2181
  admin:
    image: apache/dubbo-admin
    depends_on:
      - zookeeper
    ports:
      - 8080:8080
    environment:
      - admin.registry.address=zookeeper://zookeeper:2181
      - admin.config-center=zookeeper://zookeeper:2181
      - admin.metadata-report.address=zookeeper://zookeeper:2181

Seata

docker-compose.yaml:

version: "3"
services:
  seata-server:
    image: seataio/seata-server
    hostname: seata-server
    ports:
      - "8091:8091"
    environment:
      - SEATA_PORT=8091
      - STORE_MODE=file

MySQL

docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:latest

目錄結構

  • Storage : 商品庫存邏輯模組;
  • Account: 使用者賬戶邏輯模組;
  • Order: 商品訂單邏輯模組;
  • Business: 業務層邏輯模組;

下面我通過Storage模組來描述Dubbo + Seata的接入,其他模組,例如account, order模組的接入都是相同的。

Storage商品庫存模組

專案目錄

.
├── java
│   └── cn
│       └── mushuwei
│           └── storage
│               ├── SeataStorageApplication.java #應用SpringBoot啟動類
│               ├── api
│               │   ├── StorageApi.java #庫存呼叫Dubbo介面
│               │   └── dto
│               │       └── CommodityDTO.java #庫存資料傳輸類
│               ├── config
│               │   └── SeataAutoConfig.java #Seata配置類
│               ├── dao
│               │   └── StorageDao.java #庫存持久化類
│               ├── entity
│               │   └── StorageDO.java #庫存持久化實體
│               ├── provider
│               │   └── StorageApiImpl.java #庫存呼叫Dubbo介面實現類
│               └── service
│                   ├── StorageService.java #庫存業務操作邏輯類
│                   └── impl
│                       └── StorageServiceImpl.java #庫存業務操作邏輯實現類
└── resources
    ├── application.yml #應用配置檔案
    ├── mybatis
    │   └── storage.xml #mybatis xml檔案
    └── sql
        └── storage.sql #資料庫表結構和初始化資料

15 directories, 12 files

Pom.xml

        <!-- 日誌相關 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>log4j-over-slf4j</artifactId>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jul-to-slf4j</artifactId>
        </dependency>
				
	<!-- web服務相關 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
	<!-- mysql資料庫連線 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>

	<!-- dubbo微服務框架 -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
        </dependency>

        <!-- 使用 Zookeeper 作為註冊中心 -->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!-- seata 相關依賴-->
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </dependency>

應用配置檔案

# dubbo配置項,對應DubboConfigurationProperties 配置類
dubbo:
  application:
    name: ${spring.application.name} #應用名
  registry:
    address: zookeeper://127.0.0.1:2181 #註冊中心地址
    timeout: 1000 # 指定註冊到zk上超時時間,ms
  protocol:
    port: 20881 # 協議埠。使用 -1表示隨機埠
    name: dubbo # 使用 `dubbo://` 協議。更多協議,可見 http://dubbo.apache.org/zh-cn/docs/user/references/protocol/introduction.html 文件
  scan:
    base-packages: cn.mushuwei.storage # 指定實現服務的包
server:
  port: 8081


#資料來源配置
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/storage?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false
    username: root
    password: 123456
    type: com.alibaba.druid.pool.DruidDataSource

  application:
    name: seata-action-storage #應用名

# seata相關配置
seata:
  service:
    grouplist:
      default: 127.0.0.1:8091
    vgroup-mapping:
      service_tx_group: default
      enable-degrade: false
      disable-global-transaction: false
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: service_tx_group
  client:
    tm:
      commit-retry-count: 3
      rollback-retry-count: 3
      enable-auto-data-source-proxy: false
    rm:
      report-success-enable: true
      table-meta-check-enable: true
      report-retry-count: 5
      async-commit-buffer-limit: 1000
  transport: # Netty相關配置start
    type: TCP
    server: NIO
    heartbeat: true
    serialization: seata
    compressor: none
    enable-client-batch-send-request: true #客戶端事務訊息請求是否批量合併傳送(預設true)
    shutdown:
      wait: 3
    thread-factory:
      boss-thread-prefix: NettyBoss
      worker-thread-prefix: NettyServerNIOWorker
      server-executor-thread-prefix: NettyServerBizHandler
      share-boss-worker: false
      client-selector-thread-prefix: NettyClientSelector
      client-selector-thread-size: 1
      client-worker-thread-prefix: NettyClientWorkerThread

#資料庫sql操作列印日誌
logging:
  level:
    cn.mushuwei.storage.dao: debug

建立表結構和初始化資料

# 建立商品庫存表
create table if not exists storage.storage
(
	id bigint auto_increment
		primary key,
	commodity_code varchar(50) null comment '商品編碼',
	name varchar(255) null comment '商品名稱',
	count int null comment '商品庫存數'
);
INSERT INTO storage.storage (id, commodity_code, name, count) VALUES (1, 'cola', '可口可樂', 2000);

# 新建undo_log表
create table if not exists storage.undo_log
(
    id bigint auto_increment
        primary key,
    branch_id bigint not null,
    xid varchar(100) not null,
    context varchar(128) not null,
    rollback_info longblob not null,
    log_status int not null,
    log_created datetime not null,
    log_modified datetime not null,
    ext varchar(100) null,
    constraint ux_undo_log
        unique (xid, branch_id)
)
    charset=utf8;

將上面的sql檔案匯入到新建的storage資料庫中。這個檔案地址在resources/sql 下。

Seata配置類

package cn.mushuwei.storage.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

/**
 * @author jamesmsw
 * @date 2020/12/1 11:06 上午
 */
@Configuration
public class SeataAutoConfig {


    /**
     * autowired datasource config
     */
    @Autowired
    private DataSourceProperties dataSourceProperties;

    /**
     * init durid datasource
     *
     * @Return: druidDataSource  datasource instance
     */
    @Bean
    @Primary
    public DruidDataSource druidDataSource(){
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setUrl(dataSourceProperties.getUrl());
        druidDataSource.setUsername(dataSourceProperties.getUsername());
        druidDataSource.setPassword(dataSourceProperties.getPassword());
        druidDataSource.setDriverClassName(dataSourceProperties.getDriverClassName());
        druidDataSource.setInitialSize(0);
        druidDataSource.setMaxActive(180);
        druidDataSource.setMaxWait(60000);
        druidDataSource.setMinIdle(0);
        druidDataSource.setValidationQuery("Select 1 from DUAL");
        druidDataSource.setTestOnBorrow(false);
        druidDataSource.setTestOnReturn(false);
        druidDataSource.setTestWhileIdle(true);
        druidDataSource.setTimeBetweenEvictionRunsMillis(60000);
        druidDataSource.setMinEvictableIdleTimeMillis(25200000);
        druidDataSource.setRemoveAbandoned(true);
        druidDataSource.setRemoveAbandonedTimeout(1800);
        druidDataSource.setLogAbandoned(true);
        return druidDataSource;
    }

    /**
     * init datasource proxy
     * @Param: druidDataSource  datasource bean instance
     * @Return: DataSourceProxy  datasource proxy
     */
    @Bean
    public DataSourceProxy dataSourceProxy(DruidDataSource druidDataSource){
        return new DataSourceProxy(druidDataSource);
    }

    /**
     * init mybatis sqlSessionFactory
     * @Param: dataSourceProxy  datasource proxy
     * @Return: DataSourceProxy  datasource proxy
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSourceProxy);
        factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath*:/mybatis/*.xml"));
        factoryBean.setTransactionFactory(new JdbcTransactionFactory());
        return factoryBean.getObject();
    }
}

持久化操作

  1. StorageDao
package cn.mushuwei.storage.dao;

import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

/**
 * @author jamesmsw
 * @date 2020/11/30 7:46 下午
 */
@Repository("storageDao")
public interface StorageDao {

    /**
     * 扣減商品庫存
     *
     * @param commodityCode 商品code
     * @param count 扣減數量
     * @return
     */
    int decreaseStorage(@Param("commodityCode") String commodityCode, @Param("count") Integer count);
}

  1. Storage.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.mushuwei.storage.dao.StorageDao">

    <update id="decreaseStorage">
        update storage set count = count - #{count} where commodity_code = #{commodityCode}
    </update>
</mapper>

到此為止,商品庫存操作邏輯,就大致介紹完畢了,其他Account模組是扣減使用者餘額的操作,Order模組是新建訂單資料的,具體配置和上述描述的差不懂。

Business業務邏輯操作

package cn.mushuwei.business.controller;

import cn.mushuwei.business.dto.BusinessDTO;
import cn.mushuwei.business.service.BusinessService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @author jamesmsw
 * @date 2020/12/1 9:48 下午
 */
@RestController
@RequestMapping("/business")
@Slf4j
public class BusinessController {

    @Resource(name = "businessService")
    private BusinessService businessService;

    @PostMapping("/buy")
    public String handleBusiness(@RequestBody BusinessDTO businessDTO){
        log.info("請求引數:{}",businessDTO.toString());
        Boolean result = businessService.handleBusiness(businessDTO);
        if (result) {
            return "ok";
        }
        return "fail";
    }
}

business模組中,我們對外暴露介面/business/buy,用於給使用者進行下單操作。

業務邏輯處理

package cn.mushuwei.business.service.impl;

import cn.mushuwei.business.dto.BusinessDTO;
import cn.mushuwei.business.service.BusinessService;
import cn.mushuwei.order.api.OrderApi;
import cn.mushuwei.order.api.dto.OrderDTO;
import cn.mushuwei.storage.api.StorageApi;
import cn.mushuwei.storage.api.dto.CommodityDTO;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.stereotype.Service;

/**
 * @author jamesmsw
 * @date 2020/12/1 9:37 下午
 */
@Slf4j
@Service("businessService")
public class BusinessServiceImpl implements BusinessService {

    @DubboReference
    private StorageApi storageApi;

    @DubboReference
    private OrderApi orderApi;

    private boolean flag;

    @Override
    @GlobalTransactional(timeoutMills = 300000, name = "seata-demo-business")
    public Boolean handleBusiness(BusinessDTO businessDTO) {
        flag = true;
        log.info("開始全域性事務,XID = " + RootContext.getXID());
        CommodityDTO commodityDTO = new CommodityDTO();
        commodityDTO.setCommodityCode(businessDTO.getCommodityCode());
        commodityDTO.setCount(businessDTO.getCount());
        boolean storageResult =  storageApi.decreaseStorage(commodityDTO);

        OrderDTO orderDTO = new OrderDTO();
        orderDTO.setUserId(businessDTO.getUserId());
        orderDTO.setCommodityCode(businessDTO.getCommodityCode());
        orderDTO.setOrderCount(businessDTO.getCount());
        orderDTO.setOrderAmount(businessDTO.getAmount());
        boolean orderResult = orderApi.createOrder(orderDTO);

        //開啟註釋測試事務發生異常後,全域性回滾功能
//        if (!flag) {
//            throw new RuntimeException("測試拋異常後,分散式事務回滾!");
//        }

        if (!storageResult || !orderResult) {
            throw new RuntimeException("失敗");
        }
        return true;
    }
}

  • 我們使用@DubboReference分佈呼叫storageApiorderApi, 用於處理庫存扣減和訂單資料邏輯的操作。
  • @GlobalTransactional()在發起業務類中是必須要加的,用於全域性鎖等邏輯操作。

下單正常流程

第一階段:在正常的下單流程中,storage、order、account和business應用分別註冊到Seata這個事務協調器上,當使用者進行下單時,資料更新前後的日誌將會別記錄到每個資料庫下的undo_log表中,並形成一個全域性的鎖。以上操作全部在一個資料庫事務內完成,這樣保證了一階段操作的原子性。

第二階段: 二階段如果是提交的話,因為“業務 SQL”在一階段已經提交至資料庫, 所以 Seata 框架只需將一階段儲存的快照資料和行鎖刪掉,完成資料清理即可。

下單異常流程

第一階段:在一階段下單流程中,storage、order、account和business應用分別註冊到Seata這個事務協調器上,當使用者進行下單時,資料更新前後的日誌將會別記錄到每個資料庫下的undo_log表中,並形成一個全域性的鎖。以上操作全部在一個資料庫事務內完成,這樣保證了一階段操作的原子性。

第二階段: 當下單出現異常時,Seata將會對資料進行回滾,回滾的邏輯是按照一階段的日誌。

演示

  1. 啟動Dubbo、Seata、MySQ並初始化資料, 使各服務應用註冊到Seata上。
    • Dubbo、Seata和MySQL服務
mushuwei@mushuweideMacBook-Pro-2 seata % docker ps
CONTAINER ID        IMAGE                  COMMAND                  CREATED             STATUS              PORTS                                                  NAMES
0c9c325a039c        mysql:latest           "docker-entrypoint.s…"   2 weeks ago         Up 7 minutes        0.0.0.0:3306->3306/tcp, 33060/tcp                      mysql5.7
b8031fa865cd        seataio/seata-server   "java -Djava.securit…"   2 weeks ago         Up 20 seconds       0.0.0.0:8091->8091/tcp                                 seata_seata-server_1
2af927368a15        apache/dubbo-admin     "java -XX:+UnlockExp…"   2 weeks ago         Up 2 hours          0.0.0.0:8080->8080/tcp                                 dubbo_admin_1
7afec07234c9        zookeeper              "/docker-entrypoint.…"   2 weeks ago         Up 2 hours          2888/tcp, 3888/tcp, 0.0.0.0:2181->2181/tcp, 8080/tcp   dubbo_zookeeper_1
  • 初始化資料
mysql> use storage;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> select * from storage;
+----+----------------+------+-------+
| id | commodity_code | name | count |
+----+----------------+------+-------+
|  1 | cola           | ???? |  2000 |
+----+----------------+------+-------+
1 row in set (0.00 sec)

mysql> use account;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> select * from account;
+----+---------+---------+
| id | user_id | amount  |
+----+---------+---------+
|  1 | user123 | 1250.00 |
+----+---------+---------+
1 row in set (0.00 sec)

mysql> use order;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> select * from order;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'order' at line 1
mysql> select * from `order`;
Empty set (0.00 sec)
  • 啟動Storage、Account、Order和Business

  • Seata上各應用的註冊情況

    Starting seata_seata-server_1 ... done
    Attaching to seata_seata-server_1
    seata-server_1  | [0.001s][warning][gc] -Xloggc is deprecated. Will use -Xlog:gc:/var/log/seata_gc.log instead.
    seata-server_1  | [0.015s][info   ][gc] Using G1
    seata-server_1  | [0.841s][info   ][gc] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 14M->4M(32M) 11.654ms
    seata-server_1  | SLF4J: A number (18) of logging calls during the initialization phase have been intercepted and are
    seata-server_1  | SLF4J: now being replayed. These are subject to the filtering rules of the underlying logging system.
    seata-server_1  | SLF4J: See also http://www.slf4j.org/codes.html#replay
    seata-server_1  | 08:16:30.938  INFO --- [                     main] io.seata.server.Server                   : The server is running in container.
    seata-server_1  | 08:16:30.972  INFO --- [                     main] io.seata.config.FileConfiguration        : The file name of the operation is registry
    seata-server_1  | 08:16:30.980  INFO --- [                     main] io.seata.config.FileConfiguration        : The configuration file used is /seata-server/resources/registry.conf
    seata-server_1  | [1.385s][info   ][gc] GC(1) Pause Young (Normal) (G1 Evacuation Pause) 15M->6M(32M) 14.280ms
    seata-server_1  | 08:16:31.221  INFO --- [                     main] io.seata.config.FileConfiguration        : The file name of the operation is file.conf
    seata-server_1  | 08:16:31.222  INFO --- [                     main] io.seata.config.FileConfiguration        : The configuration file used is file.conf
    seata-server_1  | WARNING: An illegal reflective access operation has occurred
    seata-server_1  | WARNING: Illegal reflective access by net.sf.cglib.core.ReflectUtils$2 (file:/seata-server/libs/cglib-3.1.jar) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain)
    seata-server_1  | WARNING: Please consider reporting this to the maintainers of net.sf.cglib.core.ReflectUtils$2
    seata-server_1  | WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
    seata-server_1  | WARNING: All illegal access operations will be denied in a future release
    seata-server_1  | [1.734s][info   ][gc] GC(2) Pause Young (Normal) (G1 Evacuation Pause) 16M->7M(32M) 6.400ms
    seata-server_1  | [2.101s][info   ][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 18M->7M(32M) 4.828ms
    seata-server_1  | 08:16:31.924  INFO --- [                     main] i.s.core.rpc.netty.NettyServerBootstrap  : Server started, listen port: 8091
    seata-server_1  | 08:26:12.007  INFO --- [rverHandlerThread_1_1_500] i.s.c.r.processor.server.RegRmProcessor  : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/storage', applicationId='seata-action-storage', transactionServiceGroup='service_tx_group'},channel:[id: 0xae1ea1b1, L:/172.20.0.2:8091 - R:/172.20.0.1:52380],client version:1.3.0
    seata-server_1  | 08:26:12.080  INFO --- [rverHandlerThread_1_2_500] i.s.c.r.processor.server.RegRmProcessor  : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/storage', applicationId='seata-action-storage', transactionServiceGroup='service_tx_group'},channel:[id: 0xae1ea1b1, L:/172.20.0.2:8091 - R:/172.20.0.1:52380],client version:1.3.0
    seata-server_1  | 08:26:33.704  INFO --- [rverHandlerThread_1_3_500] i.s.c.r.processor.server.RegRmProcessor  : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/account', applicationId='seata-action-account', transactionServiceGroup='service_tx_group'},channel:[id: 0xd949a994, L:/172.20.0.2:8091 - R:/172.20.0.1:52396],client version:1.3.0
    seata-server_1  | 08:26:33.758  INFO --- [rverHandlerThread_1_4_500] i.s.c.r.processor.server.RegRmProcessor  : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/account', applicationId='seata-action-account', transactionServiceGroup='service_tx_group'},channel:[id: 0xd949a994, L:/172.20.0.2:8091 - R:/172.20.0.1:52396],client version:1.3.0
    seata-server_1  | 08:26:57.466  INFO --- [rverHandlerThread_1_5_500] i.s.c.r.processor.server.RegRmProcessor  : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/order', applicationId='seata-action-order', transactionServiceGroup='service_tx_group'},channel:[id: 0xfd51f88b, L:/172.20.0.2:8091 - R:/172.20.0.1:52412],client version:1.3.0
    seata-server_1  | 08:26:57.518  INFO --- [rverHandlerThread_1_6_500] i.s.c.r.processor.server.RegRmProcessor  : RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://localhost:3306/order', applicationId='seata-action-order', transactionServiceGroup='service_tx_group'},channel:[id: 0xfd51f88b, L:/172.20.0.2:8091 - R:/172.20.0.1:52412],client version:1.3.0
    seata-server_1  | 08:27:10.600  INFO --- [ettyServerNIOWorker_1_4_8] i.s.c.r.processor.server.RegTmProcessor  : TM register success,message:RegisterTMRequest{applicationId='seata-action-storage', transactionServiceGroup='service_tx_group'},channel:[id: 0x0e0b6c24, L:/172.20.0.2:8091 - R:/172.20.0.1:52424],client version:1.3.0
    seata-server_1  | 08:27:32.694  INFO --- [ettyServerNIOWorker_1_5_8] i.s.c.r.processor.server.RegTmProcessor  : TM register success,message:RegisterTMRequest{applicationId='seata-action-account', transactionServiceGroup='service_tx_group'},channel:[id: 0x2fd20474, L:/172.20.0.2:8091 - R:/172.20.0.1:52432],client version:1.3.0
    seata-server_1  | 08:27:56.453  INFO --- [ettyServerNIOWorker_1_6_8] i.s.c.r.processor.server.RegTmProcessor  : TM register success,message:RegisterTMRequest{applicationId='seata-action-order', transactionServiceGroup='service_tx_group'},channel:[id: 0xc8f6ba94, L:/172.20.0.2:8091 - R:/172.20.0.1:52436],client version:1.3.0
    seata-server_1  | 08:28:15.847  INFO --- [rverHandlerThread_1_7_500] i.s.c.r.processor.server.RegRmProcessor  : RM register success,message:RegisterRMRequest{resourceIds='null', applicationId='seata-action-business', transactionServiceGroup='service_tx_group'},channel:[id: 0x9ef75d68, L:/172.20.0.2:8091 - R:/172.20.0.1:52444],client version:1.3.0
    seata-server_1  | 08:28:15.863  INFO --- [ettyServerNIOWorker_1_7_8] i.s.c.r.processor.server.RegTmProcessor  : TM register success,message:RegisterTMRequest{applicationId='seata-action-business', transactionServiceGroup='service_tx_group'},channel:[id: 0x2b6c19d5, L:/172.20.0.2:8091 - R:/172.20.0.1:52440],client version:1.3.0
    
  1. 檢查各服務Service在Dubbo上的情況。

  1. 正常流程-模擬使用者下單,看下各應用的二階段提交日誌。
  • 執行business模組test/java目錄下的business.http檔案,對介面發起請求。
Content-Type: application/json

{
    "userId" : "user123",
    "commodityCode" : "cola",
    "count" : 2,
    "amount" : 5.0
}
  • 各資料庫資料變化

    mysql> use storage;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> select * from storage;
    +----+----------------+------+-------+
    | id | commodity_code | name | count |
    +----+----------------+------+-------+
    |  1 | cola           | ???? |  1998 |
    +----+----------------+------+-------+
    1 row in set (0.00 sec)
    
    mysql> use account;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> select * from account;
    +----+---------+---------+
    | id | user_id | amount  |
    +----+---------+---------+
    |  1 | user123 | 1245.00 |
    +----+---------+---------+
    1 row in set (0.00 sec)
    
    mysql> use order;
    Reading table information for completion of table and column names
    You can turn off this feature to get a quicker startup with -A
    
    Database changed
    mysql> select * from order;
    ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'order' at line 1
    mysql> select * from `order`;
    +----+----------------------------------+---------+------+-------+--------+
    | id | order_no                         | user_id | code | count | amount |
    +----+----------------------------------+---------+------+-------+--------+
    |  5 | dbde6ebfd72b4ad5aeba67d67ade6894 | user123 | cola |     2 |   5.00 |
    +----+----------------------------------+---------+------+-------+--------+
    1 row in set (0.00 sec)
    
    
  • 各應用下二階段提交情況,下面日誌以Storage應用為例。

    2020-12-21 16:35:41.357  INFO 5123 --- [:20881-thread-2] c.m.storage.provider.StorageApiImpl      : storage-全域性事務,XID = 172.20.0.2:8091:84324557325869056
    2020-12-21 16:35:41.431  INFO 5123 --- [:20881-thread-2] i.s.c.rpc.netty.RmNettyRemotingClient    : will register resourceId:jdbc:mysql://localhost:3306/storage
    2020-12-21 16:35:41.440  INFO 5123 --- [ctor_RMROLE_1_1] io.seata.rm.AbstractRMHandler            : the rm client received response msg [version=1.5.0-SNAPSHOT,extraData=null,identified=true,resultCode=null,msg=null] from tc server.
    2020-12-21 16:35:41.444 DEBUG 5123 --- [:20881-thread-2] c.m.s.dao.StorageDao.decreaseStorage     : ==>  Preparing: update storage set count = count - ? where commodity_code = ?
    2020-12-21 16:35:41.535 DEBUG 5123 --- [:20881-thread-2] c.m.s.dao.StorageDao.decreaseStorage     : ==> Parameters: 2(Integer), cola(String)
    2020-12-21 16:35:41.665 DEBUG 5123 --- [:20881-thread-2] c.m.s.dao.StorageDao.decreaseStorage     : <==    Updates: 1
    2020-12-21 16:35:43.345  INFO 5123 --- [h_RMROLE_1_1_16] i.s.c.r.p.c.RmBranchCommitProcessor      : rm client handle branch commit process:xid=172.20.0.2:8091:84324557325869056,branchId=84324559649513472,branchType=AT,resourceId=jdbc:mysql://localhost:3306/storage,applicationData=null
    2020-12-21 16:35:43.348  INFO 5123 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler            : Branch committing: 172.20.0.2:8091:84324557325869056 84324559649513472 jdbc:mysql://localhost:3306/storage null
    2020-12-21 16:35:43.349  INFO 5123 --- [h_RMROLE_1_1_16] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed
    2020-12-21 16:35:43.369  INFO 5123 --- [h_RMROLE_1_2_16] i.s.c.r.p.c.RmBranchCommitProcessor      : rm client handle branch commit process:xid=172.20.0.2:8091:84324557325869056,branchId=84324560404488192,branchType=AT,resourceId=jdbc:mysql://localhost:3306/storage,applicationData=null
    2020-12-21 16:35:43.369  INFO 5123 --- [h_RMROLE_1_2_16] io.seata.rm.AbstractRMHandler            : Branch committing: 172.20.0.2:8091:84324557325869056 84324560404488192 jdbc:mysql://localhost:3306/storage null
    2020-12-21 16:35:43.369  INFO 5123 --- [h_RMROLE_1_2_16] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed
    2020-12-21 16:35:43.378  INFO 5123 --- [h_RMROLE_1_3_16] i.s.c.r.p.c.RmBranchCommitProcessor      : rm client handle branch commit process:xid=172.20.0.2:8091:84324557325869056,branchId=84324560530317312,branchType=AT,resourceId=jdbc:mysql://localhost:3306/storage,applicationData=null
    2020-12-21 16:35:43.378  INFO 5123 --- [h_RMROLE_1_3_16] io.seata.rm.AbstractRMHandler            : Branch committing: 172.20.0.2:8091:84324557325869056 84324560530317312 jdbc:mysql://localhost:3306/storage null
    2020-12-21 16:35:43.378  INFO 5123 --- [h_RMROLE_1_3_16] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed
    
  1. 異常流程-模擬使用者下單,看下各應用的二階段提交日誌。

    • 修改BusinessServiceImpl類,並重啟。

          private boolean flag;
      
          @Override
          @GlobalTransactional(timeoutMills = 300000, name = "seata-demo-business")
          public Boolean handleBusiness(BusinessDTO businessDTO) {
              flag = false;
              log.info("開始全域性事務,XID = " + RootContext.getXID());
              CommodityDTO commodityDTO = new CommodityDTO();
              commodityDTO.setCommodityCode(businessDTO.getCommodityCode());
              commodityDTO.setCount(businessDTO.getCount());
              boolean storageResult =  storageApi.decreaseStorage(commodityDTO);
      
              OrderDTO orderDTO = new OrderDTO();
              orderDTO.setUserId(businessDTO.getUserId());
              orderDTO.setCommodityCode(businessDTO.getCommodityCode());
              orderDTO.setOrderCount(businessDTO.getCount());
              orderDTO.setOrderAmount(businessDTO.getAmount());
              boolean orderResult = orderApi.createOrder(orderDTO);
      
              //開啟註釋測試事務發生異常後,全域性回滾功能
              if (!flag) {
                  throw new RuntimeException("測試拋異常後,分散式事務回滾!");
              }
      
              if (!storageResult || !orderResult) {
                  throw new RuntimeException("失敗");
              }
              return true;
          }
      
    • 執行business模組test/java目錄下的business.http檔案,對介面發起請求。

    POST http://localhost:8084/business/buy
    
    HTTP/1.1 500 
    Content-Type: application/json
    Transfer-Encoding: chunked
    Date: Mon, 21 Dec 2020 08:46:24 GMT
    Connection: close
    
    {
      "timestamp": "2020-12-21T08:46:24.678+00:00",
      "status": 500,
      "error": "Internal Server Error",
      "message": "",
      "path": "/business/buy"
    }
    
  • 各應用下二階段回滾情況,下面日誌以Storage應用為例。

    2020-12-21 16:46:23.665  INFO 5123 --- [:20881-thread-6] c.m.storage.provider.StorageApiImpl      : storage-全域性事務,XID = 172.20.0.2:8091:84327252002611200
    2020-12-21 16:46:23.670 DEBUG 5123 --- [:20881-thread-6] c.m.s.dao.StorageDao.decreaseStorage     : ==>  Preparing: update storage set count = count - ? where commodity_code = ?
    2020-12-21 16:46:23.671 DEBUG 5123 --- [:20881-thread-6] c.m.s.dao.StorageDao.decreaseStorage     : ==> Parameters: 2(Integer), cola(String)
    2020-12-21 16:46:23.689 DEBUG 5123 --- [:20881-thread-6] c.m.s.dao.StorageDao.decreaseStorage     : <==    Updates: 1
    2020-12-21 16:46:24.461  INFO 5123 --- [h_RMROLE_1_7_16] i.s.c.r.p.c.RmBranchRollbackProcessor    : rm handle branch rollback process:xid=172.20.0.2:8091:84327252002611200,branchId=84327252610785280,branchType=AT,resourceId=jdbc:mysql://localhost:3306/storage,applicationData=null
    2020-12-21 16:46:24.462  INFO 5123 --- [h_RMROLE_1_7_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 172.20.0.2:8091:84327252002611200 84327252610785280 jdbc:mysql://localhost:3306/storage
    2020-12-21 16:46:24.580  INFO 5123 --- [h_RMROLE_1_7_16] i.s.r.d.undo.AbstractUndoLogManager      : xid 172.20.0.2:8091:84327252002611200 branch 84327252610785280, undo_log deleted with GlobalFinished
    2020-12-21 16:46:24.588  INFO 5123 --- [h_RMROLE_1_7_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_Rollbacked
    2020-12-21 16:46:24.596  INFO 5123 --- [h_RMROLE_1_8_16] i.s.c.r.p.c.RmBranchRollbackProcessor    : rm handle branch rollback process:xid=172.20.0.2:8091:84327252002611200,branchId=84327252556259328,branchType=AT,resourceId=jdbc:mysql://localhost:3306/storage,applicationData=null
    2020-12-21 16:46:24.596  INFO 5123 --- [h_RMROLE_1_8_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 172.20.0.2:8091:84327252002611200 84327252556259328 jdbc:mysql://localhost:3306/storage
    2020-12-21 16:46:24.610  INFO 5123 --- [h_RMROLE_1_8_16] i.s.r.d.undo.AbstractUndoLogManager      : xid 172.20.0.2:8091:84327252002611200 branch 84327252556259328, undo_log added with GlobalFinished
    2020-12-21 16:46:24.615  INFO 5123 --- [h_RMROLE_1_8_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_Rollbacked
    2020-12-21 16:46:24.621  INFO 5123 --- [h_RMROLE_1_9_16] i.s.c.r.p.c.RmBranchRollbackProcessor    : rm handle branch rollback process:xid=172.20.0.2:8091:84327252002611200,branchId=84327252489150464,branchType=AT,resourceId=jdbc:mysql://localhost:3306/storage,applicationData=null
    2020-12-21 16:46:24.621  INFO 5123 --- [h_RMROLE_1_9_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 172.20.0.2:8091:84327252002611200 84327252489150464 jdbc:mysql://localhost:3306/storage
    2020-12-21 16:46:24.634  INFO 5123 --- [h_RMROLE_1_9_16] i.s.r.d.undo.AbstractUndoLogManager      : xid 172.20.0.2:8091:84327252002611200 branch 84327252489150464, undo_log added with GlobalFinished
    2020-12-21 16:46:24.641  INFO 5123 --- [h_RMROLE_1_9_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_Rollbacked
    
    
    • 大家可以觀察到各資料庫下的資料並沒有發生變化。

以上程式碼,我已經上傳到GitHub中了,大家詳見: https://github.com/sanshengshui/seata-dubbo-action,AT模式在master分支上。

下一章將給大家介紹基於Dubbo + Seata的分散式事務 --- TCC模式的實戰案例,敬請期待!

參考文章

相關文章