1, 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.
2, Software version
Software | edition | address |
JDK | 1.8.0_271 | |
Spring Boot | 2.5.6 | |
Spring Cloud | 2020.0.4 | |
Seata | 1.4.2 |
3, Environment construction
3.1 installation of Seata
3.1.1. Download Seata server
Download seata-server-1.4.2 Zip, address: https://github.com/seata/seata/releases
3.1.2 installation of Seata server
Unzip seata-server-1.4.2 zip
3.1.3. seata database configuration
Create the database spring cloud seata Eureka and create the tables required by the seata service:
Database script required by Seata server:
https://github.com/seata/seata/tree/v1.4.2/script/server/db
Database script required by Seata client:
https://github.com/seata/seata/tree/v1.4.2/script/client/at/db
In Seata AT mode, the client only needs undo_log table. Next, create a new business table t_account,t_order,t_storage three business tables
If three tables are put into one database, you only need to create an undo_log table
If you split three tables into three databases, you need to create undo for each database_ Log table
3.1.4 configure Seata server
We need to modify the file in the seata-server-1.4.2/conf directory Conf and registry Conf, here we use the eureka registry to store the MySQL database used. The configuration is as follows
file.conf configuration is as follows:
## transaction log store, only used in seata-server store { ## store mode: file,db,redis mode = "db" ## rsa decryption public key publicKey = "" ## database store property db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc. datasource = "druid" ## mysql/oracle/postgresql/h2/oceanbase etc. dbType = "mysql" driverClassName = "com.mysql.cj.jdbc.Driver" ## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param url = "jdbc:mysql://127.0.0.1:3306/spring-cloud-seata-eureka?rewriteBatchedStatements=true&characterEncoding=utf8&useSSL=false" user = "root" password = "root" minConn = 5 maxConn = 30 globalTable = "global_table" branchTable = "branch_table" lockTable = "lock_table" queryLimit = 100 } }
registry.conf is configured as follows
registry { # file ,nacos ,eureka,redis,zk,consul,etcd3,sofa type = "eureka" eureka { serviceUrl = "http://127.0.0.1:8761/eureka" application = "default" weight = "1" } } config { # file,nacos ,apollo,zk,consul,etcd3 type = "file" file { name = "file.conf" } }
3.1.5 launch Eureka Registration Center
eureka is configured as follows:
server: port: 8761 eureka: instance: hostname: localhost client: registerWithEureka: false fetchRegistry: false serviceUrl: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
Start eureka
3.1.6 start Seata service
Execute Seata server under the seata-server-1.4.2\bin directory bat (windows)/ seata-server.sh (linux or mac) starts the Seata service
open http://localhost:8761/ , we can see that the seata service has been successfully registered in the eureka registry.
4, Project architecture
4.1 project description
Business logic for users to purchase goods. The whole business logic is supported by three micro services:
- Business service: the business logic of purchasing goods and the initiator of transactions.
- Storage service: deduct the storage quantity for a given commodity.
- Order service: create an order according to the purchase demand.
- Account service: deduct the balance from the user account.
4.2 project structure
5, Project construction
5.1 project structure
5.2 table structure initialization
Initialize three tables: order, inventory and account
-- ---------------------------- -- Table structure for t_account -- ---------------------------- DROP TABLE IF EXISTS `t_account`; CREATE TABLE `t_account` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL DEFAULT 0, `money` bigint(20) NOT NULL DEFAULT 0, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4; -- ---------------------------- -- Records of t_account -- ---------------------------- INSERT INTO `t_account` VALUES (1, 10001, 10000); -- ---------------------------- -- Table structure for t_order -- ---------------------------- DROP TABLE IF EXISTS `t_order`; CREATE TABLE `t_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL DEFAULT 0, `commodity_code` varchar(20) CHARACTER SET utf8mb4 NOT NULL DEFAULT '', `count` int(10) NOT NULL DEFAULT 0, `money` bigint(20) NOT NULL DEFAULT 0, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4; -- ---------------------------- -- Records of t_order -- ---------------------------- -- ---------------------------- -- Table structure for t_storage -- ---------------------------- DROP TABLE IF EXISTS `t_storage`; CREATE TABLE `t_storage` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `commodity_code` varchar(50) CHARACTER SET utf8mb4 NOT NULL DEFAULT '', `commodity_name` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '', `count` int(11) NOT NULL DEFAULT 0, `price` bigint(20) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `commodity_code`(`commodity_code`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4; -- ---------------------------- -- Records of t_storage -- ---------------------------- INSERT INTO `t_storage` VALUES (1, '10001', 'iPhone', 100, 100);
5.3. Service construction
5.3.1 pom dependency
POM of order service, storage service and account service Like the dependency of XML, the business service service does not need to connect to the database, so it does not need to reference mybatis plus boot starter and MySQL connector Java
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
5.3.2,application.yml
Application of order service, storage service and account service YML is basically the same, and the following areas need to be modified:
server.port spring.application.name mybatis-plus.type-aliases-package seata.application-id
Order service yml configuration
server: port: 9001 spring: application: name: order-service datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/spring-cloud-seata-eureka?characterEncoding=utf-8 username: root password: root eureka: instance: instance-id: order-service prefer-ip-address: true client: fetch-registry: true register-with-eureka: true service-url: defaultZone: http://127.0.0.1:8761/eureka mybatis-plus: global-config: banner: false configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #Open sql log map-underscore-to-camel-case: true # Open hump type-aliases-package: com.seata.order.entity #Define the package of aliases of all operation classes mapper-locations: classpath:mapper/*Mapper.xml # Seata Config seata: application-id: order-service tx-service-group: my_test_tx_group service: vgroup-mapping: # The configuration here corresponds to the Server side configuration registry eureka. Value of application my_test_tx_group: default registry: type: eureka eureka: service-url: http://localhost:8761/eureka weight: 1
The business service service does not need to connect to the database, so there is no need to configure the datasource and mybatis plus nodes
server: port: 9000 spring: application: name: business-service eureka: instance: instance-id: business-service prefer-ip-address: true client: fetch-registry: true register-with-eureka: true service-url: defaultZone: http://127.0.0.1:8761/eureka # Seata Config seata: application-id: business-service tx-service-group: my_test_tx_group service: vgroup-mapping: # The configuration here corresponds to the Server side configuration registry eureka. Value of application my_test_tx_group: default registry: type: eureka eureka: service-url: http://localhost:8761/eureka weight: 1
5.3.3. seata configuration description (this step does not need to be configured, but just explain the above configuration)
registry. The content in conf has been configured to application YML, so you don't need to reference registry Conf file
If you don't want to apply To configure the content related to seata in YML, you only need to file the script related to seata conf,registry. Copy conf to the project resource directory
File address: https://github.com/seata/seata/tree/v1.4.2/script/client/conf
Where: Registry Conf is modified as follows:
registry { # file ,nacos ,eureka,redis,zk,consul,etcd3,sofa,custom type = "eureka" eureka { serviceUrl = "http://localhost:8761/eureka" weight = "1" } } config { # file,nacos ,apollo,zk,consul,etcd3,springCloudConfig,custom type = "file" file { name = "file.conf" } }
5.3.4 warehousing services
/** * Deduct inventory */ int deductStorage(String commodityCode, int count); /** * Deduct inventory * * @param commodityCode Commodity code * @param count quantity * @return */ @Override @Transactional(rollbackFor = Exception.class) public int deductStorage(String commodityCode, int count) { log.info("[Inventory service]>------>Start of inventory deduction"); storageMapper.deductStorage(commodityCode, count); log.info("[Inventory service]>------>End of inventory deduction"); return count; }
5.3.5 order service
/** * Create order */ Long createOrder(Long userId, String commodityCode, int count); /** * Create order * * @param userId * @param commodityCode * @param count * @return */ @Override public Long createOrder(Long userId, String commodityCode, int count) { log.info("[Order service]>------>Create order start"); //Deduction of account balance Long price = storageService.selectPrice(commodityCode); Long money = price * count; // Create order Order order = new Order(); order.setUserId(userId); order.setCommodityCode(commodityCode); order.setMoney(money); order.setCount(count); orderMapper.insert(order); // Deduction account log.info("[Order service]>------>Start of deduction account"); accountService.deductAccount(order.getUserId(), money); log.info("[Order service]>------>End of account deduction"); log.info("[Order service]>------>End of order creation"); return order.getId(); }
5.3.6 account services
/** * Deduction account */ Long deductAccount(Long userId, Long money); /** * Deduction account * @param userId * @param money * @return */ @Override public Long deductAccount(Long userId, Long money) { log.info("[Account services]>------>Start of deduction account"); if (10000 == userId) { throw new RuntimeException("[Inventory service]>------>Deduction inventory exception"); } accountMapper.deductAccount(userId, money); log.info("[Account services]>------>Failed to deduct account"); return money; }
5.3.7 purchase business logic
/** * Deduct inventory - create order * * @param userId User Id * @param commodityCode Commodity code * @param count quantity */ @Override @GlobalTransactional(timeoutMills = 10000, name = "spring-cloud-seata", rollbackFor = Exception.class) public Long purchase(Long userId, String commodityCode, int count) { log.info("Start global transaction, XID = " + RootContext.getXID()); log.info("[Procurement services]>------>Start of inventory deduction"); storageService.deductStorage(commodityCode, count); log.info("[Procurement services]>------>End of inventory deduction"); log.info("[Procurement services]>------>Create order start"); Long orderId = orderService.createOrder(userId, commodityCode, count); log.info("[Procurement services]>------>End of order creation"); return orderId; }
5.3.8. Check service startup
order
5.3 distributed transaction test
5.3.1 normal business logic test
postman test:
Request address: http://localhost:9000/business/purchase?userId=10001&commodityCode=10001&count=1
In the database: there is already a piece of data in the order table, and the inventory quantity of goods in the inventory table is reduced by 1
business
order
5.3.1 abnormal business logic test