"springcloud 2021 series" Seata completely solves the problem of distributed transactions

Posted by ksukat on Mon, 14 Feb 2022 05:10:22 +0100

Distributed transaction

Monomer application

In a single application, a business operation needs to call three modules to complete. At this time, the consistency of data is guaranteed by local transactions.

Microservice application

With the change of business requirements, individual applications are divided into micro service applications. The original three modules are divided into three independent applications, which use 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.

In the microservice architecture, the problem that global data consistency cannot be guaranteed is the problem of distributed transactions. In short, if a business operation needs to operate multiple data sources or make remote calls, the problem of distributed transactions will arise.

Introduction to 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.

Seata Solve the distributed transaction problem in the microservice scenario in an efficient and business 0 intrusive way.

Three components of the protocol distributed transaction processing process:

  • 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 submission or rollback

A typical distributed transaction process:

  • TM applies to TC to start a global transaction. The global transaction is successfully created and a globally unique XID is generated
  • XID propagates in the context of the microservice invocation link
  • RM registers branch transactions with TC and brings them under the jurisdiction of global transactions corresponding to XID
  • TM initiates a global commit or rollback resolution for XID to TC
  • TC schedules all branch transactions under XID to complete the commit or rollback request

Spring Cloud support

  • Service providers that provide services through Spring MVC can automatically restore the Seata context when they receive an HTTP request containing Seata information in the header

  • Support the service caller to automatically pass the Seata context when calling through RestTemplate

  • Support the service caller to automatically pass the Seata context when calling through FeignClient

  • Support the scenario of using SeataClient and Hystrix at the same time

  • Support the scenario of using SeataClient and Sentinel at the same time

Seata installation

docker-compose

  • Use Nacos as the registry

  • Prepare registry Conf file

    Register seata service to nacos registry; Specify the configuration mode of seata as file, that is, file type

registry {
  type = "nacos"

  nacos {
  # seata service is an alias registered on nacos, through which the client invokes the service
    application = "seata-server"
  # Specifies the name of the group registered with the nacos registry
    group = "SEATA_GROUP"
    
  # Please configure the ip and port of nacos service according to the actual production environment
    serverAddr = "192.168.123.22:8848"
  # namespace specified on nacos
    namespace = ""
  # Specify the cluster name registered with the nacos registry  
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
}

config {
  type = "file"

  file {
    name="file:/root/seata-config/file.conf"
  }
}
  • Prepare file Conf file

    Configure the storage mode to db, and specify the host, user, password, etc. of the database, More configuration

# Storage mode
store.mode=db

store.db.datasource=druid
store.db.dbType=mysql

# The driverClassName needs to be adjusted according to the version of mysql
# driver corresponding to mysql8 and above: com mysql. cj. jdbc. driver
# mysql8 or later driver: com mysql. jdbc. Driver
store.db.driverClassName=com.mysql.cj.jdbc.Driver

# Pay attention to adjusting the parameters host and port according to the actual production situation
store.db.url="jdbc:mysql://192.168.123.22:3306/seata-server?useUnicode=true&characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true"

# Database user name
store.db.user=test

# User name and password
store.db.password=testMysql

store.mode is a db mode. You need to create a corresponding table structure in the database Seata server[ [create table Script]

Create a Seata server database, and the sql table is located in Doc / Seata / DB_ store. In mysql, take MySQL as an example:

-- 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(128),
      `transaction_id` BIGINT,
      `branch_id`      BIGINT       NOT NULL,
      `resource_id`    VARCHAR(256),
      `table_name`     VARCHAR(32),
      `pk`             VARCHAR(36),
      `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
      `gmt_create`     DATETIME,
      `gmt_modified`   DATETIME,
      PRIMARY KEY (`row_key`),
      KEY `idx_status` (`status`),
      KEY `idx_branch_id` (`branch_id`)
  ) ENGINE = InnoDB
    DEFAULT CHARSET = utf8;
  
  CREATE TABLE IF NOT EXISTS `distributed_lock`
  (
      `lock_key`       CHAR(20) NOT NULL,
      `lock_value`     VARCHAR(20) NOT NULL,
      `expire`         BIGINT,
      primary key (`lock_key`)
  ) ENGINE = InnoDB
    DEFAULT CHARSET = utf8mb4;
  
  INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
  INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
  INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
  INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
  • Prepare docker compose Yaml file

Set Doc / seata / conf / registry Conf and file Put conf in the / mnt/volume/seata/config folder. nacos and seata containers are configured as follows:

version: '3.5'

services:
  nacos-registry:
    image: nacos/nacos-server:2.0.3
    container_name: nacos-registry
    #command:
    restart: always
    #https://nacos.io/zh-cn/docs/quick-start-docker.html
    environment:
      - "MODE=standalone"
    ports:
      - 8848:8848

  # http://seata.io/zh-cn/docs/ops/deploy-by-docker-compose.html
  # chmod 777 /mnt/volume/seata
  seata:
    image: seataio/seata-server:1.4.2
    container_name: seata
    environment:
      # Specify seata service startup port
      - SEATA_PORT=8091
      # ip registered to nacos. The client will access the seata service through this ip
      - SEATA_IP=192.168.123.22
      - SEATA_CONFIG_NAME=file:/root/seata-config/registry
    restart: always
    volumes:
      # Because registry Conf is the nacos configuration center. You only need to set Doc / Seata / conf / registry Conf and file Conf in / mnt/volume/seata/config folder
      - /mnt/volume/seata/conf:/root/seata-config
      - /mnt/volume/seata/logs:/root/logs
    ports:
      - "8091:8091"
  • Start container

Run docker compose - f docker compose env YML up - d start the nacos and seata containers

You can see that Seata server has successfully registered with the nacos registry

environment variable

Seata server supports the following environment variables:

  • SEATA_IP

Optional, specify the IP of Seata server startup, which is used when registering with the registry

  • SEATA_PORT

Optional. Specify the port on which Seata server starts. The default is 8091

  • STORE_MODE

Optional. Specify the transaction log storage method of Seata server. DB, file and redis are supported (supported by Seata server version 1.3 and above). The default is file

  • SERVER_NODE

Optional, used to specify the Seata server node ID, such as 1, 2, 3... Which is generated by ip by default

  • SEATA_ENV

Optional. Specify the running environment of Seata server, such as dev, test, etc. the configuration of registry-dev.conf will be used when the service is started

  • SEATA_CONFIG_NAME

Optional. Specify the location of the configuration file, such as file:/root/registry, which will load / root / registry Conf as the configuration file. If necessary, specify file Conf file, you need to set registry Conf file. The value of name is changed to something like file: / root / file conf:

Project practice

Source code: https://github.com/langyastudio/langya-tech/tree/master/spring-cloud

Database preparation

  • Business database

    • Seat order: the database that stores the order
    • Seat storage: a database that stores inventory
    • Seat account: a database that stores account information
  • Log rollback table

    Each business database needs to add a log rollback table

    The Seata AT mode needs to use the log rollback table undo_log table

The complete SQL is as follows:

-- seat-order: Database where orders are stored
create database `seat-order`;
use `seat-order`;

CREATE TABLE `seat-order`.`order_tbl`
(
    `id`             int(11) NOT NULL AUTO_INCREMENT,
    `user_id`        varchar(255) DEFAULT NULL COMMENT 'user id',
    `commodity_code` varchar(255) DEFAULT NULL COMMENT 'Commodity code',
    `count`          int(11)      DEFAULT 0 COMMENT 'quantity',
    `money`          int(11)      DEFAULT 0 COMMENT 'amount of money',
    `status`         int(1)       DEFAULT NULL COMMENT 'Order status: 0: being created; 1: Closed',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- Seata AT The mode requires the use of log rollback tables undo_log surface
-- Note here 0.3.0+ Unique index increase ux_undo_log
CREATE TABLE `seat-order`.`undo_log`
(
    `id`            bigint(20)   NOT NULL AUTO_INCREMENT,
    `branch_id`     bigint(20)   NOT NULL,
    `xid`           varchar(100) NOT NULL,
    `context`       varchar(128) NOT NULL,
    `rollback_info` longblob     NOT NULL,
    `log_status`    int(11)      NOT NULL,
    `log_created`   datetime     NOT NULL,
    `log_modified`  datetime     NOT NULL,
    `ext`           varchar(100) DEFAULT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8;


-- seat-storage: Database for storing inventory
create database `seat-storage`;
use `seat-storage`;

CREATE TABLE `seat-storage`.`storage_tbl`
(
    `id`             int(11) NOT NULL AUTO_INCREMENT,
    `commodity_code` varchar(255) DEFAULT NULL COMMENT 'Commodity code',
    `count`          int(11)      DEFAULT 0 COMMENT 'quantity',
    PRIMARY KEY (`id`),
    UNIQUE KEY (`commodity_code`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;
INSERT INTO `seat-storage`.`storage_tbl` (`id`, `commodity_code`, `count`)
VALUES (1, 'C00321', 100);

-- Seata AT The mode requires the use of log rollback tables undo_log surface
-- Note here 0.3.0+ Add unique index ux_undo_log
CREATE TABLE `seat-storage`.`undo_log`
(
    `id`            bigint(20)   NOT NULL AUTO_INCREMENT,
    `branch_id`     bigint(20)   NOT NULL,
    `xid`           varchar(100) NOT NULL,
    `context`       varchar(128) NOT NULL,
    `rollback_info` longblob     NOT NULL,
    `log_status`    int(11)      NOT NULL,
    `log_created`   datetime     NOT NULL,
    `log_modified`  datetime     NOT NULL,
    `ext`           varchar(100) DEFAULT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8;


-- seat-account: Database for storing account information
create database `seat-account`;
use `seat-account`;

CREATE TABLE `seat-account`.`account_tbl`
(
    `id`      int(11) NOT NULL AUTO_INCREMENT,
    `user_id` varchar(255) DEFAULT NULL COMMENT 'user id',
    `money`   int(11)      DEFAULT 0 COMMENT 'amount of money',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;
INSERT INTO `seat-account`.`account_tbl` (`id`, `user_id`, `money`)
VALUES (1, 'U100001', 1000);

-- Seata AT The mode requires the use of log rollback tables undo_log surface
-- Note here 0.3.0+ Unique index increase ux_undo_log
CREATE TABLE `seat-account`.`undo_log`
(
    `id`            bigint(20)   NOT NULL AUTO_INCREMENT,
    `branch_id`     bigint(20)   NOT NULL,
    `xid`           varchar(100) NOT NULL,
    `context`       varchar(128) NOT NULL,
    `rollback_info` longblob     NOT NULL,
    `log_status`    int(11)      NOT NULL,
    `log_created`   datetime     NOT NULL,
    `log_modified`  datetime     NOT NULL,
    `ext`           varchar(100) DEFAULT NULL,
    PRIMARY KEY (`id`),
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8;

Schematic diagram of complete database:

Client configuration

Configure three seata clients: seata order service, seata storage service and seata account service. Their configurations are roughly the same. Take the configuration of seata order service as an example

  • Modify application YML file

    • Database connection url, account and password

    • Name of custom transaction group

    • nacos registration configuration

spring:
  application:
    name: order-service

  datasource:
    name: '"orderDataSource"'
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.123.22:3306/seat-order?useSSL=false&serverTimezone=UTC
    username: test
    password: testMysql
    druid:
      initial-size: 2
      max-active: 20
      min-idle: 2

  cloud:
    nacos:
      discovery:
        server-addr: 192.168.123.22:8848
        username: naocs
        password: nacos

    alibaba:
      seata:
        #The user-defined transaction group name needs to correspond to that in Seata server
        tx-service-group: business-service

seata:
  enabled: true
  service:
    disable-global-transaction: false
    grouplist:
      default: 192.168.123.22:8091
    vgroup-mapping:
      #Transaction group name = cluster name
      #The cluster name must be consistent with the cluster registered with Nacos by Seata server
      business-service: default

  # if use registry center
  registry:
    nacos:
      cluster: default
      server-addr: 192.168.123.22
      username: nacos
      password: nacos
    type: nacos
  • Modify global transactions

    Use the @ GlobalTransactional annotation to start distributed transactions, such as

    @GlobalTransactional(timeoutMills = 300000, name = "spring-cloud-business-tx")
    

Transaction issues

Three services will be created here, one order service, one inventory service and one account service. This operation spans three databases and has two remote calls. It is obvious that there will be distributed transaction problems.

  • When the user places an order, an order will be created in the order service
  • Then the inventory service is called remotely to deduct the inventory of the ordered goods
  • And then deduct the balance in the user's account by calling the account service remotely
  • Finally, modify the order status to completed in the order service

Transaction presentation

Through the @ GlobalTransactional parameter, all service data can be executed or not executed

If distributed transactions take effect, the following equation should hold:

  • User's original amount (1000) = user's existing amount + unit price of goods (2) * order quantity * quantity of goods per order (2)

  • Initial quantity of goods (100) = on hand quantity of goods + order quantity * quantity of goods per order (2)

In this example, a scenario in which a user purchases goods is simulated. Seata storage service is responsible for deducting the inventory quantity, Seata order service is responsible for saving the order, and Seata account service is responsible for deducting the balance of the user's account.

To demonstrate the example, use random. In Seata account service The method of nextboolean() is used to throw exceptions randomly, which simulates the scenario of random exceptions during service invocation.

  • Run Seata order service, Seata storage service, Seata account service and Seata business service

  • After calling the interface to place an order, view the database: http://localhost:18081/seata/feign

    Because the account service throws an exception, it can be found that there is no change in the database data after the order is placed

  • Mask the throw new RuntimeException exception in Seata account service, and then continue the interface

    @GetMapping(value = "/account", produces = "application/json")
    public String account(String userId, int money)
    {
        --------
        if (random.nextBoolean())
        {
            //throw new RuntimeException("this is a mock Exception");
        }
    	-----   
    }
    

Execute normally and find that the data in the database changes

reference resources

Official documents of Seata

Use Seata to completely solve the problem of distributed transactions in Spring Cloud!

Topics: Distribution Spring Cloud Microservices