Seata distributed transaction

Posted by ddragas on Sun, 20 Feb 2022 04:45:20 +0100

Seata

Seata is an open source distributed transaction solution, which is committed to providing high-performance and easy-to-use distributed transaction services. Seata will provide users with AT, TCC, SAGA and XA transaction modes to create a one-stop distributed solution for users.

Evolution of the two-stage submission agreement:

Phase I: the business data and rollback log records are committed in the same local transaction, and the local lock and connection resources are released.

Phase II: commit asynchronously and complete very quickly. Rollback is compensated in reverse through the rollback log of phase I.

Environment construction

  1. Download address https://github.com/seata/seata/releases/download/v1.4.0/seata-server-1.4.0.zip
  2. Unzip the zip file, focusing on the conf and bin directories
  3. conf directory

registry.conf

At present, the registry supports nacos, eureka, redis, zk, consumer, etcd3 and sofa. We can choose one we are familiar with.

At present, the configuration center supports file, nacos, apollo, zk, consul t and etcd3. We can choose one we are familiar with, generally use file, and then modify file conf.

file.conf

Storage can be set in it. The default type is = "file".

If db is selected, the configuration can be as follows:

  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.jdbc.Driver"
    url = "jdbc:mysql://192.168.33.10:3306/seata"
    user = "zhexiao"
    password = "password"
    minConn = 5
    maxConn = 100
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }

Create globalTable (persistent global transaction), branchTable (persistent transaction of each submitted branch) and lockTable (persistent transaction of locking resources of each branch) tables in the corresponding database.

CREATE TABLE `global_table` (
  `xid` VARCHAR(128) NOT NULL,
  `transaction_id` BIGINT(20) DEFAULT NULL,
  `status` TINYINT(4) NOT NULL,
  `application_id` VARCHAR(32) DEFAULT NULL,
  `transaction_service_group` VARCHAR(32) DEFAULT NULL,
  `transaction_name` VARCHAR(128) DEFAULT NULL,
  `timeout` INT(11) DEFAULT NULL,
  `begin_time` BIGINT(20) DEFAULT NULL,
  `application_data` VARCHAR(2000) DEFAULT NULL,
  `gmt_create` DATETIME DEFAULT NULL,
  `gmt_modified` DATETIME DEFAULT NULL,
  PRIMARY KEY (`xid`),
  KEY `idx_gmt_modified_status` (`gmt_modified`,`status`),
  KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

-- Persistent transactions of each committed branch
CREATE TABLE `branch_table` (
  `branch_id` BIGINT(20) NOT NULL,
  `xid` VARCHAR(128) NOT NULL,
  `transaction_id` BIGINT(20) DEFAULT NULL,
  `resource_group_id` VARCHAR(32) DEFAULT NULL,
  `resource_id` VARCHAR(256) DEFAULT NULL,
  `branch_type` VARCHAR(8) DEFAULT NULL,
  `status` TINYINT(4) DEFAULT NULL,
  `client_id` VARCHAR(64) DEFAULT NULL,
  `application_data` VARCHAR(2000) DEFAULT NULL,
  `gmt_create` DATETIME(6) DEFAULT NULL,
  `gmt_modified` DATETIME(6) DEFAULT NULL,
  PRIMARY KEY (`branch_id`),
  KEY `idx_xid` (`xid`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

-- Persist each branch lock table transaction
CREATE TABLE `lock_table` (
  `row_key` VARCHAR(128) NOT NULL,
  `xid` VARCHAR(96) DEFAULT NULL,
  `transaction_id` BIGINT(20) DEFAULT NULL,
  `branch_id` BIGINT(20) NOT NULL,
  `resource_id` VARCHAR(256) DEFAULT NULL,
  `table_name` VARCHAR(32) DEFAULT NULL,
  `pk` VARCHAR(36) DEFAULT NULL,
  `gmt_create` DATETIME DEFAULT NULL,
  `gmt_modified` DATETIME DEFAULT NULL,
  PRIMARY KEY (`row_key`),
  KEY `idx_branch_id` (`branch_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

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,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
  1. bin directory startup

Start the service seata-server-1.4.0 \ Seata \ bin > Seata server bat

The following is displayed to indicate successful startup:

Spring cloud configuration

  1. Parent POM xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.sc</groupId>
    <artifactId>sc-scaffold</artifactId>
    <version>0.1</version>

    <modules>
        <module>sc-config</module>
        <module>sc-gateway</module>
        <module>sc-common</module>
        <module>sc-test</module>
        <module>sc-eureka</module>
        <module>sc-apps</module>
        <module>sc-apps/app1</module>
        <module>sc-apps/app2</module>
    </modules>

    <name>${project.artifactId}</name>
    <packaging>pom</packaging>

    <properties>
        <spring-boot.version>2.4.0</spring-boot.version>
        <spring-cloud.version>2020.0.0</spring-cloud.version>
        <spring-cloud-alibaba.version>2020.0.RC1</spring-cloud-alibaba.version>

        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>

        <fastjson.version>1.2.75</fastjson.version>
        <swagger.fox.version>3.0.0</swagger.fox.version>
    </properties>

    <profiles>
        <profile>
            <id>dev</id>
            <properties>
                <!-- Environment ID, which should correspond to the name of the configuration file -->
                <profiles.active>dev</profiles.active>
            </properties>
            <activation>
                <!-- Default environment -->
                <activeByDefault>true</activeByDefault>
            </activation>
        </profile>
    </profiles>

    <dependencies>
        <!--bootstrap starter-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
        <!--Profile processor-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
        <!--monitor-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--journal-->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
        </dependency>
        <!--Lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--Test dependency-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--JSON-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <!-- spring boot rely on -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- spring cloud rely on -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- spring cloud alibaba rely on -->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--spring cloud Component dependency-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
                <version>3.0.0</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
                <version>3.0.0</version>
            </dependency>

            <!--jwt-->
            <dependency>
                <groupId>com.auth0</groupId>
                <artifactId>java-jwt</artifactId>
                <version>3.15.0</version>
            </dependency>

            <!--database-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.20</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.2.0</version>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.1.4</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <!--appoint filtering=true.maven The placeholder parsing expression can be used for the files in it-->
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>

        <plugins>
            <!--support yaml read pom Parameters of-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>3.2.0</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                    <delimiters>
                        <delimiter>@</delimiter>
                    </delimiters>
                    <useDefaultDelimiters>false</useDefaultDelimiters>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
  1. Sub POM xml

Load the corresponding core dependency.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>sc-scaffold</artifactId>
        <groupId>com.sc</groupId>
        <version>0.1</version>
        <relativePath>../../pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>app1</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.sc</groupId>
            <artifactId>sc-core</artifactId>
            <version>0.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--Service registration-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!--Configuration center client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>

        <!--database-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--seata-spring-boot-starter The version installed is the same as the version of the service-->
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.4.0</version>
        </dependency>

        <!--feign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!-- loadbalancer -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  1. application.yml

Load the corresponding core configuration.

server:
  port: 3001

spring:
  application:
    name: @artifactId@
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848 # Server address of nacos
        group: SEATA_GROUP
    config:
      discovery:
        enabled: true
        service-id: sc-config
      name: ${spring.application.name},spring-base
      profile: @profiles.active@
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://mysql-host:3306/db01?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2B8
    username: zhexiao
    password: password

mybatis:
  mapper-locations: classpath:mybatis/mapper/*.xml
  configuration:
    mapUnderscoreToCamelCase: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

seata:
  enabled: true
  application-id: seata-app1 #Unique identification of each application
  tx-service-group: app_tx_test_group #This group and service Vgroup mapping consistent
  registry:
    type: nacos
    nacos:
      application: seata-server
      group: SEATA_GROUP
      server-addr: 127.0.0.1:8848
      cluster: default
      username: nacos
      password: nacos
  service:
    vgroup-mapping:
      app_tx_test_group: default
  1. Start APP project

The following logs are printed, which basically indicates that the connection is successful.

test

App1 and app2 start two microservices for testing, in which app1 is the user service and app2 is the storage service.

Microservice table structure for testing

The structure of the table is as follows:

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(200) DEFAULT NULL,
  `money` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8

CREATE TABLE `storage` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(200) DEFAULT NULL,
  `count` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8

Tested Service Roles

We take user service as the main service, and storage is the service operated by user.

The user service connects to the storage service through feign:

/**
 * @author zhe.xiao
 * @date 2021-04-30 14:18
 * @description
 */
@Component
@FeignClient(value = "ab-app2")
public interface StorageFeign {
    @GetMapping(path = "/storage/change")
    public void change(@RequestParam int count);

    @GetMapping(path = "/storage/add")
    public void insert(@RequestParam int count);
}

user service:

package com.sc.app1.service;


import com.sc.app1.entity.User;
import com.sc.app1.feign.StorageFeign;
import com.sc.app1.mapper.UserMapper;
import com.sc.core.exception.ApiException;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

/**
 * @author zhe.xiao
 * @date 2021-04-30 13:58
 * @description
 */
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    UserMapper userMapper;

    @Autowired
    StorageFeign storageFeign;


    @GlobalTransactional(rollbackFor = Exception.class)
    @Override
    public void change(int money, int storageCount, int orderNum) {
        User user = new User();
        user.setId(1L);
        user.setMoney(money);

        userMapper.update(user);

        storageFeign.change(storageCount);

        if(storageCount == 0){
            throw new ApiException("storageCount = 0");
        }
    }

    @GlobalTransactional(rollbackFor = Exception.class)
    @Override
    public void add(int money, int storageCount, int orderNum) {
        User user = new User();
        user.setName("Xiao Zhe" + LocalDateTime.now());
        user.setMoney(money);

        userMapper.insert(user);

        storageFeign.insert(storageCount);

        if(storageCount == 0){
            throw new ApiException("storageCount = 0");
        }
    }
}

controller:

package com.sc.app1.controller;


import com.sc.app1.service.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author zhe.xiao
 * @date 2021-04-30 13:59
 * @description
 */
@RestController
@RequestMapping(path = "user")
public class UserController {
    @Autowired
    UserServiceImpl userService;

    @GetMapping(path = "/change")
    public void change(
            @RequestParam int money,
            @RequestParam int storageCount,
            @RequestParam int orderNum

    )  {
        userService.change(money, storageCount, orderNum);
    }

    @GetMapping(path = "/add")
    public void add(
            @RequestParam int money,
            @RequestParam int storageCount,
            @RequestParam int orderNum

    )  {
        userService.add(money, storageCount, orderNum);
    }
}

Test results:

Visit: localhost: 2000 / API app1 / user / add? money=500&storageCount=10&orderNum=1000

Normal submission

Visit: localhost: 2000 / API app1 / user / add? money=500&storageCount=0&orderNum=1000

Normal rollback

Topics: Java Distribution