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
Use Seata to completely solve the problem of distributed transactions in Spring Cloud!