There are many theories about distributed transactions on the Internet, and few practical ones. Today, I want to make friends feel distributed transactions through a case. Let’s try to talk less about theories today. Our hero today is Seata!
Distributed transaction involves a lot of theories, such as CAP, BASE, etc., many friends just see these theories are persuaded, so we will not talk about the theory today, let’s see a Demo, through the code to quickly experience what is distributed transaction.
1. What is Seata?
Seata is an open source distributed transaction solution dedicated 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.
The four transaction modes supported by Seata are:
- Seata AT mode
- Seata TCC mode
- Seata Saga mode
- Seata XA mode
There are three core concepts in Seata:
- Transaction Coordinator (TC) – Transaction Coordinator: maintains the status of global and branch transactions and drives global Transaction commit or rollback.
- TM (Transaction Manager) – Transaction Manager: Defines the scope of a global Transaction to start, commit, or roll back a global Transaction.
- RM (Resource Manager) – Resource Manager: Manages resources for branch transaction processing, talks to TCS to register branch transactions and report branch transaction status, and drives branch transaction commit or rollback.
TCS are servers deployed separately, and TM and RM are clients embedded in applications.
These concepts can be understood as a small group of friends, do not understand Seata can also be used, understand more Seata working principle.
2. Set up the Seata server
Let’s start by setting up the Seata server.
Seata download address:
- Github.com/seata/seata…
The latest version is 1.4.2, so we’ll work with the latest version.
It doesn’t make much difference whether the tool is deployed on Windows or Linux, so I’ll deploy it directly on Windows for convenience.
We first download the 1.4.2 zip package, download and decompress it, and then configure two places in the conf directory:
- First, configure the file.conf file
Conf to configure the storage mode for TCS. There are three storage modes for TCS:
- File: Suitable for single-machine deployment mode, global transaction session information is read and written in memory, and the local file root.data is persisted, resulting in high performance.
- Db: Suitable for cluster mode. Global transaction session information is shared through DB, which results in poor performance.
- Redis: The redis mode is supported in SEATa-Server 1.3 and above, and the performance is higher, but there is a risk of transaction information loss. Therefore, developers need to configure the redis persistence configuration in advance to suit the current scenario.
In this case, the transaction session information is read and written in memory, and the persistent information is written to the local file, as shown in the figure below:
If db or Redis mode is configured, please fill in the following information. The details are as follows:
digression
Note that if you use db mode, you need to prepare the database script in advance, as follows (friends can directly download the database script in the public account Jiangnan Little Rain background reply seata-db) :
CREATE DATABASE / *! 32312 IF NOT EXISTS*/`seata2` / *! 40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ / *! 80016 DEFAULT ENCRYPTION='N' */;
USE `seata2`;
/*Table structure for table `branch_table` */
DROP TABLE IF EXISTS `branch_table`;
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;
/*Data for the table `branch_table` */
/*Table structure for table `global_table` */
DROP TABLE IF EXISTS `global_table`;
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;
/*Data for the table `global_table` */
/*Table structure for table `lock_table` */
DROP TABLE IF EXISTS `lock_table`;
CREATE TABLE `lock_table` (
`row_key` varchar(128) NOT NULL,
`xid` varchar(128) 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;
Copy the code
Mysql5.x and mysql8.x provide the corresponding database driver (in the lib directory), we only need to change the driver on the line.
- Then configure the registry. Conf file
Registry. Conf is used to configure Seata’s registry. Eureka is used to configure Seata’s registry.
As you can see, there are many configuration centers supported. We select Eureka. After selecting the configuration center, remember to modify the information about the configuration center.
OK, now the configuration is complete, but don’t start it yet, there is still one Eureka registry left.
3. Configure the project
Next we configure the project.
Seata official provided a very classic Demo, let’s look at the Demo directly.
The official case can be downloaded at github.com/seata/seata…
But here is a lot of cases mixed together, may look more messy, and because to download more dependence, so it is very likely to rely on download failure, so you can also reply in the public account background seata-demo to get songg sorted out the case, directly imported, as shown below:
This is a commodity order case, let me explain to you a little bit:
- Eureka: This is the service registry.
- Account: This is the account service, can query/modify the user’s account information (mainly account balance).
- Order: This is an order service. You can place an order.
- Storage: This is a storage service that can query/modify the quantity of goods in stock.
- Bussiness: This is business, where the user orders will be done.
What’s the story here?
When the user wants to place an order, the bussiness interface is called. The bussiness interface calls its own service. In the service, the global distributed transaction is enabled first, and then the storage interface is called through feIGN to delete the inventory. The order is then created by calling the order interface through feign (order not only creates the order, but also deducts the balance of the user’s account). After the inventory is deducted and the order is created, the user is then checked to see if the balance and inventory are correct. If the user balance is negative or the amount of inventory is negative, the transaction is rolled back, otherwise the transaction is committed.
The specific structure of this case is as follows:
This case is a typical distributed transaction problem. The transactions in storage and Order belong to different microservices, but we want them to succeed or fail at the same time.
Now that you understand what this case is about, let’s run it.
Start by creating a database named SEata, and then execute the all.sql data script from the above code.
Next, open the above project with idea, and modify the connection information of data in the application. Properties file of each project (Eureka is not changed), as shown in the figure below:
Except for Eureka, the other four need to be changed.
OK, the configuration is complete.
4. Start the test
Start Eureka first.
Next, before you remember to start other services, start Seata Server, which is the service we configured in the second section, in its bin directory, Windows double-click /Linux to execute the startup script.
Finally, start the remaining four services separately. After startup, we can view the relevant information in Eureka:
As you can see, the various services are registered.
Next, we access the two test interfaces provided in Bussiness.
The first test interface is:
http://127.0.0.1:8084/purchase/commit
The code for this interface is: IO. Seata. Sample. Controller. BusinessController# purchaseCommit, this place is to simulate U100000 user bought 30 C100000 goods, the price of each item is 100. The inventory of goods is 200 and the balance of the user account is 10000, so after the purchase, the inventory of goods becomes 170 and the balance of the user account becomes 7000. This is the case with normal purchases.
@RequestMapping(value = "/purchase/commit", produces = "application/json")
public String purchaseCommit(a) {
try {
businessService.purchase("U100000"."C100000".30);
} catch (Exception exx) {
return exx.getMessage();
}
return "Global Transaction Commit";
}
Copy the code
When we are done with the interface, we can go to the database to see the corresponding data.
The interface for the second test is:
http://127.0.0.1:8084/purchase/rollback
The code for this interface is: IO. Seata. Sample. Controller. BusinessController# purchaseRollback, this time is to simulate the user to purchase 99999 goods, whether user account balance or inventory quantity, cannot support the purchasing behavior, Therefore, the call to this interface will eventually be rolled back, leaving the data in the database intact.
@RequestMapping("/purchase/rollback")
public String purchaseRollback(a) {
try {
businessService.purchase("U100000"."C100000".99999);
} catch (Exception exx) {
return exx.getMessage();
}
return "Global Transaction Commit";
}
Copy the code
This is a distributed transaction case.
Interested friends can also study the official this case, we will find that the things here are very simple, just one more comment on the following method (IO) seata. Sample. Service. BusinessService# purchase) :
@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {
storageFeignClient.deduct(commodityCode, orderCount);
orderFeignClient.create(userId, commodityCode, orderCount);
if(! validData()) {throw new RuntimeException("Insufficient account or inventory, perform rollback"); }}Copy the code
The purchase transactional method uses the @globalTransactional annotation to open a global transaction. The purchase transactional method uses the @globalTransactional annotation to open a global transaction. The purchase transactional method uses the @globalTransactional annotation to open a global transaction. The code that has been executed above is rolled back.
The rest of the code for this project is normal code in microservices and won’t be covered here.
5. Implementation principle
Let’s talk a little bit about distributed transactions in Seata. Let’s look ata diagram:
This diagram clearly describes the above case, and the general process is as follows:
- There are three concepts: TM, RM, and TC, which we have already introduced in section 1 and will not repeat here.
- The global transaction is first started by Business.
- Next, when Business calls Storage and Order, both register a branch transaction with the TC and commit it before the database operation.
- A branch transaction commits a record to the UNdo_log table during operation. When a global transaction commits, records in the undo_log table will be cleared. Otherwise, reverse compensation will be made based on records in this table (data will be restored as is).
In the above case, transaction commit is divided into two phases, and the process is as follows:
A phase:
- First, Business opens a global transaction. During this process, it registers with the TC and then gets an XID, which is a global transaction ID.
- Next, the Storage microservice is invoked in Business.
- To parse SQL: get information about SQL type (UPDATE), table (storage_tbl), condition (where commodity_code = ‘C100000’), etc.
- Mirroring before query: Generates a query statement based on the obtained condition information to locate the data.
- Perform business SQL, that is, do real data update operations.
- Mirror after query: Locate data using the primary key based on the results of the previous mirror.
- Insert rollback log: A rollback log record is composed of the mirror data before and after and the information related to the service SQL, and inserted into the UNDO_LOG table.
Branch_id = branch transaction id; xID = global transaction ID; rollback_info = branch transaction id; xID = branch transaction ID; xID = global transaction ID; Songge picks out the most important part of this JSON to share with you:
- BeforeImage: This is the data in the database before the change. You can see the value of each field, id is 4, and count is 200.
- AfterImage = afterImage = afterImage = afterImage = afterImage = afterImage = afterImage = afterImage
- Before submission, the Storage registers branches with the TC to apply for global locks for records whose primary key value is equal to 4 in the Storage_TBL table.
- Local transaction commit: Updates to the business data are committed together with the UNDO LOG generated in the previous step.
- Similarly, the Order and Account submit data according to the above steps.
The steps 1-10 above are one-phase data commit.
Let’s look at the second stage:
Phase two has two possibilities, commit or rollback.
Take the above example again:
@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {
storageFeignClient.deduct(commodityCode, orderCount);
orderFeignClient.create(userId, commodityCode, orderCount);
if(! validData()) {throw new RuntimeException("Insufficient account or inventory, perform rollback"); }}Copy the code
When placing an order, the inventory is deducted and the order is created. In the last check, it is found that the inventory is negative or the balance of the user account is negative, indicating that there is a problem with the order. At this time, it should throw an exception and roll back; otherwise, the data will be submitted.
The specific operations are as follows:
Roll back:
- After receiving a branch rollback request from a TC, start a local transaction and perform the following operations:
- Use xID and branch_id to find the corresponding record in undo_log table.
- Data verification: Compares the rearview found in the second step with the current data. If there is a difference, the data is modified by an action other than the current global transaction. This situation needs to be handled according to the configuration policy.
- If the comparison of the third step is the same, the rollback statement is generated and executed based on the information about the pre-image and the business SQL in undo_log.
- Commit the local transaction. The execution result of the local transaction (that is, the branch transaction rollback result) is reported to the TC.
Commit:
- After receiving a branch submission request from a TC, put the request into an asynchronous task queue and return a successful submission result to the TC immediately.
- Branch commit requests in the asynchronous task phase will asynchronously and in batches delete the corresponding UNDO LOG records.
In other words, if the transaction commits normally, there is no record in the undo_log table. If you want to see the record in the undo_log table, you can DEBUG the record before the transaction commits.
6. Summary
Is that the end of Seata? NONONO!!! This is just AT mode! There are three modes, which will be shared in the next article.
Ok, this is a simple distributed transaction, friends to feel first! The title is five minutes to feel a distributed transaction, because the article inside I also share with you the principle, if you just run a case feeling, five minutes should be enough, do not believe to try!