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
- Download address https://github.com/seata/seata/releases/download/v1.4.0/seata-server-1.4.0.zip
- Unzip the zip file, focusing on the conf and bin directories
- 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;
- 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
- 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>
- 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>
- 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
- 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