The concepts related to transactions will not be introduced. This blog first demonstrates local transactions and distributed transactions, and then introduces the basic use of Seata. Later, it will introduce Seata's cluster, registry, configuration center and various transaction modes.
Construction works
A parent module and a child module. The parent module is used to manage dependent versions, and the child module is used to demonstrate local transactions and distributed transactions (demonstrated through a simple version of a user's purchase case). Finally, it is necessary to integrate the Seata distributed transaction solution.
POM of parent module 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> <groupId>com.kaven</groupId> <artifactId>alibaba</artifactId> <packaging>pom</packaging> <version>1.0-SNAPSHOT</version> <description>Spring Cloud Alibaba</description> <modules> <module>seata</module> </modules> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> <spring-cloud-version>Hoxton.SR9</spring-cloud-version> <spring-cloud-alibaba-version>2.2.6.RELEASE</spring-cloud-alibaba-version> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.2.RELEASE</version> </parent> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud-version}</version> <type>pom</type> <scope>import</scope> </dependency> <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> </dependencies> </dependencyManagement> </project>
seata module
seata module demonstrates local transactions and distributed transactions through a simple product purchase case. The structure of seata module is shown in the following figure:
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"> <parent> <groupId>com.kaven</groupId> <artifactId>alibaba</artifactId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>seata</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.26</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> </project>
application.yml:
server: port: 9000 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: ITkaven@666 url: jdbc:mysql://localhost:3306/seata?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai jpa: show-sql: true
Entity
Order entity class:
package com.kaven.alibaba.entity; import lombok.Data; import javax.persistence.*; @Entity @Table(name = "`order`") // You cannot change 'order' to order (Mysql database keyword), otherwise an error will occur when interacting with the database @Data public class Order { // Order id @Id @GeneratedValue(strategy = GenerationType.IDENTITY) public Integer id; // User id public Integer userId; // Commodity id public Integer productId; // Purchase quantity of goods public Integer count; // Order amount public Integer money; }
Storage entity class:
package com.kaven.alibaba.entity; import lombok.Data; import javax.persistence.*; @Entity @Table(name = "storage") @Data public class Storage { // Commodity id @Id @GeneratedValue(strategy = GenerationType.IDENTITY) public Integer id; // stock public Integer count; }
User entity class:
package com.kaven.alibaba.entity; import lombok.Data; import javax.persistence.*; @Entity @Table(name = "user") @Data public class User { // User id @Id @GeneratedValue(strategy = GenerationType.IDENTITY) public Integer id; // User balance public Integer money; }
Repository
OrderRepository:
package com.kaven.alibaba.repository; import com.kaven.alibaba.entity.Order; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface OrderRepository extends JpaRepository<Order, Integer> {}
StorageRepository:
package com.kaven.alibaba.repository; import com.kaven.alibaba.entity.Storage; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface StorageRepository extends JpaRepository<Storage, Integer> {}
UserRepository:
package com.kaven.alibaba.repository; import com.kaven.alibaba.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface UserRepository extends JpaRepository<User, Integer> {}
Service
IOrderService:
package com.kaven.alibaba.service; public interface IOrderService { void create(int userId, int productId, int count, int money); }
OrderServiceImpl:
package com.kaven.alibaba.service.impl; import com.kaven.alibaba.entity.Order; import com.kaven.alibaba.repository.OrderRepository; import com.kaven.alibaba.service.IOrderService; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class OrderServiceImpl implements IOrderService { @Resource private OrderRepository orderRepository; @Override public void create(int userId, int productId, int count, int money) { // Generate order Order order = new Order(); order.setUserId(userId); order.setProductId(productId); order.setCount(count); order.setMoney(money); orderRepository.save(order); } }
IStorageService:
package com.kaven.alibaba.service; public interface IStorageService { void deduct(int productId, int count); }
StorageServiceImpl:
package com.kaven.alibaba.service.impl; import com.kaven.alibaba.entity.Storage; import com.kaven.alibaba.repository.StorageRepository; import com.kaven.alibaba.service.IStorageService; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.Optional; @Service public class StorageServiceImpl implements IStorageService { @Resource private StorageRepository storageRepository; @Override public void deduct(int productId, int count) { Optional<Storage> byId = storageRepository.findById(productId); if(byId.isPresent()) { Storage storage = byId.get(); if(storage.getCount() >= count) { // Inventory reduction storage.setCount(storage.getCount() - count); storageRepository.save(storage); } else { throw new RuntimeException("This item is out of stock!"); } } else { throw new RuntimeException("The product does not exist!"); } } }
IUserService:
package com.kaven.alibaba.service; public interface IUserService { void debit(int userId, int money); }
UserServiceImpl:
package com.kaven.alibaba.service.impl; import com.kaven.alibaba.entity.User; import com.kaven.alibaba.repository.UserRepository; import com.kaven.alibaba.service.IUserService; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.Optional; @Service public class UserServiceImpl implements IUserService { @Resource private UserRepository userRepository; @Override public void debit(int userId, int money) { Optional<User> byId = userRepository.findById(userId); if(byId.isPresent()) { User user = byId.get(); if(user.getMoney() >= money) { // Less balance user.setMoney(user.getMoney() - money); userRepository.save(user); } else { throw new RuntimeException("The user's balance is insufficient!"); } } else { throw new RuntimeException("There is no such user!"); } } }
IBuyService:
package com.kaven.alibaba.service; public interface IBuyService { void buy(int userId, int productId, int count); }
package com.kaven.alibaba.service.impl; import com.kaven.alibaba.service.IBuyService; import com.kaven.alibaba.service.IOrderService; import com.kaven.alibaba.service.IStorageService; import com.kaven.alibaba.service.IUserService; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class BuyServiceImpl implements IBuyService { @Resource private IOrderService orderService; @Resource private IStorageService storageService; @Resource private IUserService userService; @Override public void buy(int userId, int productId, int count) { int money = count; // Generate order orderService.create(userId, productId, count, money); // Inventory reduction storageService.deduct(productId, count); // Less balance userService.debit(userId, money); } }
Controller
BuyController:
package com.kaven.alibaba.controller; import com.kaven.alibaba.service.IBuyService; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController public class BuyController { @Resource private IBuyService buyService; @PostMapping("/buy") public String buy(@RequestParam("userId") Integer userId, @RequestParam("productId") Integer productId, @RequestParam("count") Integer count) { buyService.buy(userId, productId, count); return "success"; } }
Startup class:
package com.kaven.alibaba; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class); } }
database
Enter the database and execute the following sql (for simplicity, no relevant index is created).
CREATE DATABASE seata; USE seata; DROP TABLE IF EXISTS `storage`; CREATE TABLE `storage` ( `id` int(11) NOT NULL AUTO_INCREMENT, `count` int(11) DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `order`; CREATE TABLE `order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) DEFAULT NULL, `product_id` int(11) DEFAULT NULL, `count` int(11) DEFAULT 0, `money` int(11) DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `money` int(11) DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Insert several pieces of data, as shown in the following figure:
Start the application.
Local transaction
Send a request to the application using Postman.
Obviously, the user's amount is not enough.
Inventories of goods have also been deducted.
The order has been created.
The operation of reducing user balance has not been performed, so the user balance remains unchanged, which leads to the problem of inconsistent data.
This kind of transaction under only one service is easy to solve, and Spring's @ Transactional annotation can be used.
package com.kaven.alibaba.service.impl; import com.kaven.alibaba.service.IBuyService; import com.kaven.alibaba.service.IOrderService; import com.kaven.alibaba.service.IStorageService; import com.kaven.alibaba.service.IUserService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; @Service public class BuyServiceImpl implements IBuyService { @Resource private IOrderService orderService; @Resource private IStorageService storageService; @Resource private IUserService userService; @Override @Transactional public void buy(int userId, int productId, int count) { int money = count; // Generate order orderService.create(userId, productId, count, money); // Inventory reduction storageService.deduct(productId, count); // Less balance userService.debit(userId, money); } }
Restart the application and manually roll back the data in the database. Then use Postman to send a request to the application.
The exception chain information is different from before. In fact, it is the function of @ Transactional annotation. The principle of this annotation will be introduced by bloggers in the Spring source code reading series in the future.
At this time, the data is consistent.
Distributed transaction
Now turn each operation into an interface to simulate a distributed environment.
Controller
OrderController:
package com.kaven.alibaba.controller; import com.kaven.alibaba.service.IOrderService; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController @RequestMapping("/order") public class OrderController { @Resource private IOrderService orderService; @PostMapping("/create") public void create(@RequestParam("userId") Integer userId, @RequestParam("productId") Integer productId, @RequestParam("count") Integer count, @RequestParam("money") Integer money) { orderService.create(userId, productId, count, money); } }
StorageController:
package com.kaven.alibaba.controller; import com.kaven.alibaba.service.IStorageService; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController @RequestMapping("/storage") public class StorageController { @Resource private IStorageService storageService; @PostMapping("/deduct") public void deduct(@RequestParam("productId") Integer productId, @RequestParam("count") Integer count) { storageService.deduct(productId, count); } }
UserController:
package com.kaven.alibaba.controller; import com.kaven.alibaba.service.IUserService; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @RestController @RequestMapping("/user") public class UserController { @Resource private IUserService userService; @PostMapping("/debit") public void debit(@RequestParam("userId") Integer userId, @RequestParam("money") Integer money) { userService.debit(userId, money); } }
Modify BuyController:
package com.kaven.alibaba.controller; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; @RestController public class BuyController { @Resource private RestTemplate restTemplate; @PostMapping("/buy") @Transactional public String buy(@RequestParam("userId") Integer userId, @RequestParam("productId") Integer productId, @RequestParam("count") Integer count) { // Request parameters MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(); queryParams.add("userId", userId.toString()); queryParams.add("productId", productId.toString()); queryParams.add("count", count.toString()); queryParams.add("money", count.toString()); // Construction request UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("http://localhost:9000/order/create").queryParams(queryParams); restTemplate.postForObject(builder.toUriString(), null, Void.class); // Construction request builder = UriComponentsBuilder.fromHttpUrl("http://localhost:9000/storage/deduct").queryParams(queryParams); restTemplate.postForObject(builder.toUriString(), null, Void.class); // Construction request builder = UriComponentsBuilder.fromHttpUrl("http://localhost:9000/user/debit").queryParams(queryParams); restTemplate.postForObject(builder.toUriString(), null, Void.class); return "success"; } }
Restart the application, and then use Postman to send a request to the application.
Inconsistent data.
The @ Transactional annotation will not work in the distributed environment (for simplicity, this is just a simulation of the case of users purchasing goods in the distributed environment). Next, you need to use Seata to solve this problem.
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 (these introductions about Seata are from the official website).
- TC (Transaction Coordinator): maintains the status of global and branch transactions and drives global transaction commit or rollback.
- TM (Transaction Manager): define the scope of global transactions, start global transactions, commit or roll back global transactions.
- RM (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.
Obviously, TM and RM are in business code, while TC is a separate application provided by Seata.
Bloggers download 1.3 Version 0 (to be compatible with Spring Cloud Alibaba version).
For simple use, there is no need to modify the configuration (the default form is based on the local file. For simplicity, the default form needs to be used. Other forms need to be used in the distributed environment, which will be introduced by bloggers later). Start Seata:
C:\Users\Dell>f: F:\>cd F:\tools\seata-server-1.3.0\seata\bin F:\tools\seata-server-1.3.0\seata\bin>dir Driver F The volume in is WorkSpace The serial number of the volume is D671-D29B F:\tools\seata-server-1.3.0\seata\bin Directory of 2021/12/29 10:50 <DIR> . 2020/07/16 00:35 <DIR> .. 2020/07/16 00:35 3,648 seata-server.bat 2020/07/16 00:35 4,175 seata-server.sh 2021/12/29 10:51 <DIR> sessionStore 2 Files 7,823 byte 3 Directories 241,159,860,224 Available bytes F:\tools\seata-server-1.3.0\seata\bin>seata-server.bat -p 8080
To modify a profile:
server: port: 9000 spring: application: name: seata datasource: driver-class-name: com.mysql.cj.jdbc.Driver username: root password: ITkaven@666.com url: jdbc:mysql://47.112.7.219:3306/seata?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai jpa: show-sql: true seata: application-id: buy tx-service-group: kaven_seata_tx_group service: vgroup-mapping: kaven_seata_tx_group: default grouplist: - "127.0.0.1:8080"
file.conf (the service related configuration here should be consistent with that in the application.yml configuration file):
transport { # tcp udt unix-domain-socket type = "TCP" #NIO NATIVE server = "NIO" #enable heartbeat heartbeat = true # the client batch send request enable enableClientBatchSendRequest = true #thread factory for netty threadFactory { bossThreadPrefix = "NettyBoss" workerThreadPrefix = "NettyServerNIOWorker" serverExecutorThread-prefix = "NettyServerBizHandler" shareBossWorker = false clientSelectorThreadPrefix = "NettyClientSelector" clientSelectorThreadSize = 1 clientWorkerThreadPrefix = "NettyClientWorkerThread" # netty boss thread size,will not be used for UDT 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.kaven_seata_tx_group = "default" #only support when registry.type=file, please don't set multiple addresses default.grouplist = "127.0.0.1:8080" #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 reportSuccessEnable = false } tm { commitRetryCount = 5 rollbackRetryCount = 5 } undo { dataValidation = true logSerialization = "jackson" logTable = "undo_log" } log { exceptionRate = 100 } }
registry.conf:
registry { # file ,nacos ,eureka,redis,zk,consul,etcd3,sofa type = "file" file { name = "file.conf" } } config { # file,nacos ,apollo,zk,consul,etcd3 type = "file" file { name = "file.conf" } }
Annotate the buy interface with @ GlobalTransactional:
@PostMapping("/buy") @GlobalTransactional public String buy(@RequestParam("userId") Integer userId, @RequestParam("productId") Integer productId, @RequestParam("count") Integer count) { // Request parameters MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>(); queryParams.add("userId", userId.toString()); queryParams.add("productId", productId.toString()); queryParams.add("count", count.toString()); queryParams.add("money", count.toString()); // Construction request UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("http://localhost:9000/order/create").queryParams(queryParams); restTemplate.postForObject(builder.toUriString(), null, Void.class); // Construction request builder = UriComponentsBuilder.fromHttpUrl("http://localhost:9000/storage/deduct").queryParams(queryParams); restTemplate.postForObject(builder.toUriString(), null, Void.class); // Construction request builder = UriComponentsBuilder.fromHttpUrl("http://localhost:9000/user/debit").queryParams(queryParams); restTemplate.postForObject(builder.toUriString(), null, Void.class); return "success"; }
Create undo in database_ Log table.
USE seata; DROP TABLE IF EXISTS `undo_log`; 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, `ext` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
Start the application.
Manually roll back the data in the database, and then use Postman for testing.
Client logs:
TC log:
The data in the database is also consistent.
It can also be seen from these log information that the application of Seata distributed transaction solution is successful. The demonstration of local transaction and distributed transaction and the use of Seata are introduced here. If the blogger has something wrong or you have different opinions, you are welcome to comment and supplement.