Spring Cloud Alibaba Seata distributed transaction solution

Posted by netbros on Sat, 12 Feb 2022 13:22:00 +0100

1, Distributed transaction problem

  • A business operation needs to be called remotely across multiple data sources or across multiple systems, which will lead to distributed transaction problems.

give an example:

Business logic for users to purchase goods. The whole business logic is supported by three micro services:

  • Warehousing service: deduct the warehousing quantity for a given commodity.
  • Order service: create orders according to purchase requirements.
  • Account service: deduct the balance from the user account.

The single application is split into micro service applications. The original three modules are split into three independent applications, which use three independent data sources respectively. Business operations need to call three services to complete. At this time, the data consistency within each service is guaranteed by local transactions, but the global data consistency cannot be guaranteed.

2, Introduction to Seata

Seata official website address: http://seata.io/zh-cn/

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

Seata terminology:

  • TC (Transaction Coordinator) - Transaction Coordinator
    Maintain the status of global and branch transactions and drive global transaction commit or rollback.

  • TM (Transaction Manager) - transaction manager
    Define the scope of global transaction: start global transaction, commit or roll back global transaction.

  • RM (Resource Manager) - Resource Manager
    Manage the resources of branch transaction processing, talk with TC to register branch transactions and report the status of branch transactions, and drive branch transaction commit or rollback.

Seata process:

  1. TM applies to TC to start a global transaction, which is successfully created and generates a globally unique XID;
  2. XID propagates in the context of microservice call link;
  3. RM registers branch transactions with TC and brings them under the jurisdiction of global transactions corresponding to XID;
  4. TM initiates a global commit or rollback resolution for XID to TC;
  5. TC schedules all branch transactions under XID to complete the commit or rollback request

3, Seata deployment

Version selection of Seata: Correspondence between Spring Cloud Alibaba, Spring Boot and Spring Cloud versions

Official Website Version Description

This time: Seata 1.2.0

3.1 configuration of Seata server

Official configuration document: Seata Deployment Guide

Download the source code of seata-server-1.2.0 and seata-1.2.0

  • Seate server download: https://seata.io/zh-cn/blog/download.html
  • seata-1.2.0 source code download: https://github.com/seata/seata/releases

3.1.1 modify configuration file

After decompression, enter the conf directory to start parameter configuration. We modify the file Conf and registry Conf these two files.

1. Enter the conf folder and modify the file Conf file

2. Modify registry Conf file

  • If config is set to file, you do not need to set the configuration of nacos on the Internet

3.1.2 MySQL database configuration

We first create the database seata (the database should correspond to the db setting in file.conf), and the database table creation statement is in the server connection of README file

Let me execute mysql sql

-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
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;

-- the table to store BranchSession data
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;

-- the table to store lock data
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;

3.1.3 start the Seata Server side

3.2 configuration of Seata client

3.2.1 business pre preparation

Business scenario: business logic for users to purchase goods. The whole business logic is supported by three micro services:

User A purchases goods, calls A service to create order completion, calls B service to deduct inventory, and then calls C service to deduct account balance. The data consistency within each service is guaranteed by local transactions, multiple service calls to complete the business, and the global transaction data consistency is guaranteed by Seata.

  • Order service A: create orders according to purchase requirements.

  • Warehousing service B: deduct the warehousing quantity for a given commodity.

  • Account Service C: deduct the balance from the user account.

**Business database preparation: * * configure three businesses corresponding to their respective databases.

  • Database corresponding to service A: seata_order ; Table: t_order
  • Database corresponding to service B: seata_storage ; Table: t_storage
  • C service corresponding database: seata_account ; Table: t_account

Create table statement:

# Create seata_order database
CREATE DATABASE seata_order;

# Create t_order table
CREATE TABLE seata_order.t_order(
    `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `user_id` BIGINT(11) DEFAULT NULL COMMENT 'user id',
    `product_id` BIGINT(11) DEFAULT NULL COMMENT 'product id',
    `count` INT(11) DEFAULT NULL COMMENT 'quantity',
    `money` DECIMAL(11,0) DEFAULT NULL COMMENT 'amount of money',
    `status` INT(1) DEFAULT NULL COMMENT 'Order status: 0: Creating; 1: Closed'
) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
# Create seata_storage database
CREATE DATABASE seata_storage;

# Create t_storage table
CREATE TABLE seata_storage.t_storage(
    `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    `product_id` BIGINT(11) DEFAULT NULL COMMENT 'product id',
    `total` INT(11) DEFAULT NULL COMMENT 'Total inventory',
    `used` INT(11) DEFAULT NULL COMMENT 'Used inventory',
    `residue` INT(11) DEFAULT NULL COMMENT 'Remaining inventory'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

# Insert a piece of data 
INSERT INTO seata_storage.t_storage(`id`,`product_id`,`total`,`used`,`residue`)
VALUES('1','1','100','0','100');
# Create seata_account database
CREATE DATABASE seata_account;

# Create t_account table
CREATE TABLE seata_account.t_account(
    `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
    `user_id` BIGINT(11) DEFAULT NULL COMMENT 'user id',
    `total` DECIMAL(10,0) DEFAULT NULL COMMENT 'Total amount',
    `used` DECIMAL(10,0) DEFAULT NULL COMMENT 'Balance used',
    `residue` DECIMAL(10,0) DEFAULT '0' COMMENT 'Remaining available limit'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

# Insert a piece of data 
INSERT INTO seata_account.t_account(`id`,`user_id`,`total`,`used`,`residue`) VALUES('1','1','1000','0','1000');

3.2.2 creating undo_log table

Create the corresponding rollback log table according to the above three libraries, and the table creation file is in readme_ client in Zh file

undo_ The statement of creating the log table is as follows:

-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `id`            BIGINT(20)   NOT NULL AUTO_INCREMENT COMMENT 'increment id',
    `branch_id`     BIGINT(20)   NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(100) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME     NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME     NOT NULL COMMENT 'modify datetime',
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';

Completion diagram:

The following configuration takes seata-order-service2001 as an example

  • Project structure:

3.2.3 Seata Client configuration file

Refer to readme for the preparation of configuration files_ client in Zh file:

1,file.conf, modify this

transport {
  # tcp, unix-domain-socket
  type = "TCP"
  #NIO, NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  # the tm client batch send request enable
  enableTmClientBatchSendRequest = false
  # the rm client batch send request enable
  enableRmClientBatchSendRequest = true
   # the rm client rpc request timeout
  rpcRmRequestTimeout = 2000
  # the tm client rpc request timeout
  rpcTmRequestTimeout = 10000
  # the tc client rpc request timeout
  rpcTcRequestTimeout = 5000
  #thread factory for netty
  threadFactory {
    bossThreadPrefix = "NettyBoss"
    workerThreadPrefix = "NettyServerNIOWorker"
    serverExecutorThread-prefix = "NettyServerBizHandler"
    shareBossWorker = false
    clientSelectorThreadPrefix = "NettyClientSelector"
    clientSelectorThreadSize = 1
    clientWorkerThreadPrefix = "NettyClientWorkerThread"
    # netty boss thread size
    bossThreadSize = 1
    #auto default pin or 8
    workerThreadSize = "default"
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
  #transaction service group mapping
  vgroupMapping.my_test_tx_group = "default"
  #only support when registry.type=file, please don't set multiple addresses
  default.grouplist = "127.0.0.1:8091"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

client {
  rm {
    asyncCommitBufferLimit = 10000
    lock {
      retryInterval = 10
      retryTimes = 30
      retryPolicyBranchRollbackOnConflict = true
    }
    reportRetryCount = 5
    tableMetaCheckEnable = false
    tableMetaCheckerInterval = 60000
    reportSuccessEnable = false
    sagaBranchRegisterEnable = false
    sagaJsonParser = "fastjson"
    sagaRetryPersistModeUpdate = false
    sagaCompensatePersistModeUpdate = false
    tccActionInterceptorOrder = -2147482648 #Ordered.HIGHEST_PRECEDENCE + 1000
  }
  tm {
    commitRetryCount = 5
    rollbackRetryCount = 5
    defaultGlobalTransactionTimeout = 60000
    degradeCheck = false
    degradeCheckPeriod = 2000
    degradeCheckAllowTimes = 10
    interceptorOrder = -2147482648 #Ordered.HIGHEST_PRECEDENCE + 1000
  }
  undo {
    dataValidation = true
    onlyCareUpdateColumns = true
    logSerialization = "jackson"
    logTable = "undo_log"
    compress {
      enable = true
      # allow zip, gzip, deflater, 7z, lz4, bzip2, zstd default is zip
      type = zip
      # if rollback info size > threshold, then will be compress
      # allow k m g t
      threshold = 64k
    }
  }
  loadBalance {
      type = "RandomLoadBalance"
      virtualNodes = 10
  }
}
log {
  exceptionRate = 100
}
tcc {
  fence {
    # tcc fence log table name
    logTableName = tcc_fence_log
    # tcc fence log clean period
    cleanPeriod = 1h
  }
}

2,registry.conf, modify this

registry {
  # file ,nacos ,eureka,redis,zk,consul,etcd3,sofa,custom
  type = "nacos"

  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    username = ""
    password = ""
    ##if use MSE Nacos with auth, mutex with username/password attribute
    #accessKey = ""
    #secretKey = ""
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = "0"
    password = ""
    timeout = "0"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  consul {
    serverAddr = "127.0.0.1:8500"
    aclToken = ""
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
  custom {
    name = ""
  }
}

config {
  # file,nacos ,apollo,zk,consul,etcd3,springCloudConfig,custom
  type = "file"

  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = ""
    password = ""
    ##if use MSE Nacos with auth, mutex with username/password attribute
    #accessKey = ""
    #secretKey = ""
    dataId = "seata.properties"
  }
  consul {
    serverAddr = "127.0.0.1:8500"
	key = "seata.properties"
    aclToken = ""
  }
  apollo {
    appId = "seata-server"
    apolloMeta = "http://192.168.1.204:8801"
    namespace = "application"
    apolloAccesskeySecret = ""
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
    nodePath = "/seata/seata.properties"
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
    key = "seata.properties"
  }
  file {
    name = "file.conf"
  }
  custom {
    name = ""
  }
}

3.2.4 pom.xml file

<dependencies>
    <!--nacos-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--seata-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        <exclusions>
            <exclusion>
                <artifactId>seata-all</artifactId>
                <groupId>io.seata</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-all</artifactId>
        <version>1.2.0</version>
    </dependency>
    <!--feign-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--web-actuator-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--mysql-druid-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.37</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.10</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.0.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

3.2.5 application.yml file

server:
  port: 2001

spring:
  application:
    name: seata-order-service
  cloud:
    alibaba:
      seata:
        # The user-defined transaction group name needs to correspond to that in Seata server
        tx-service-group: my_test_tx_group #Because seata's file There is no service module in the conf file, and the default transaction group name is my_test_tx_group
        #Service should be aligned with TX service group. vgroupMapping and grouplist are at the next level of service, my_test_tx_group is at the next level
        service:
          vgroupMapping:
            #Be consistent with the value of TX service group
            my_test_tx_group: default
          grouplist:
            # Address configuration of seata seaver. Cluster configuration here is an array
            default: 127.0.0.1:8091
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
    username: root
    password: root

feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata:

mybatis:
  mapperLocations: classpath:mapper/*.xml

3.2.6 business code writing: add @ GlobalTransactional

The code of dao layer, mapper layer and controller layer is omitted

The business code uses OpenFeign to call between services to realize a simple business function. AT mode uses only one @ GlobalTransactional annotation to implement distributed transactions. The name attribute is a unique representation of transactions and can be defined AT will. The rollback for property specifies an Exception before the transaction is rolled back.

OrderServiceImpl.java

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    @Resource
    private OrderDao orderDao;

    @Resource
    private StorageService storageService;

    @Resource
    private AccountService accountService;

    /**
     * Create order - > call inventory service to deduct inventory - > call account service to deduct account balance - > Modify order status
     * Simply put:
     * Place order - > reduce inventory - > reduce balance - > change status
     */
    @Override
    @GlobalTransactional(name = "fsp-create-order",rollbackFor = Exception.class)
    public void create(Order order) {
        log.info("------->Order start");
        //Create order for this application
        orderDao.create(order);

        //Remote call inventory service to deduct inventory
        log.info("------->order-service Start of inventory deduction");
        storageService.decrease(order.getProductId(),order.getCount());
        log.info("------->order-service End of inventory deduction");

        //Remote call account service deduction balance
        log.info("------->order-service Start of deduction balance in");
        accountService.decrease(order.getUserId(),order.getMoney());
        log.info("------->order-service End of deduction balance in");

        //Modify order status to completed
        log.info("------->order-service Start modifying order status in");
        orderDao.update(order.getUserId(),0);
        log.info("------->order-service End of modifying order status in");

        log.info("------->End of order");
    }
}

3.2.7 config code writing

1,MybatisConfig.java

@Configuration
@MapperScan({"com.zb.springcloud.dao"})
public class MyBatisConfig {
}

2,DataSourceProxyConfig.java

package com.zb.springcloud.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

@Configuration
public class DataSourceProxyConfig {

    @Value("${mybatis.mapperLocations}")
    private String mapperLocations;

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource(){
        return new DruidDataSource();
    }

    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }

}

3.2.8 main startup

@EnableDiscoveryClient
@EnableFeignClients
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//Cancel automatic creation of data source
public class SeataOrderMainApp2001 {

    public static void main(String[] args)
    {
        SpringApplication.run(SeataOrderMainApp2001.class, args);
    }
}

3.3 testing

  • Start the three services respectively. You can see that the Seata Sever server will give corresponding prompts during the project startup

Send request http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100 To simulate an order request. After sending multiple requests, you can see that the order has been placed successfully, and there are transactions in the process of processing on the Seata Server side. (at this time, there is no data in the database because the transaction is completed)

Reference blog:

Topics: Spring Cloud seata