1. What is distributed transaction
First, this is general business:
The following are distributed transactions:
In a microservice system, each microservice application may have its own database. They first need to control their own local transactions.
A business operation may call and execute multiple microservices. How to ensure the overall success or failure of multiple database operations performed by multiple services? This is the problem to be solved by distributed transactions.
2. Theoretical part
CAP and BASE are the theoretical summary of the distributed practice of large-scale Internet system.
CAP
CAP principle
In the distributed system, the partition will be caused when the subsystems cannot communicate due to the network. This situation must be tolerated in general distributed systems, so we need to choose between A and C.
In distributed transactions
- If CP is guaranteed, it means that the data operations of all subsystems are either successful or failed, and inconsistency is not allowed. However, strong consistency will cause performance degradation.
- If the AP is guaranteed, it means that certain consistency can be sacrificed to allow some data operations to succeed and some data operations to fail in each subsystem, as long as the final consistency can be achieved through subsequent processing.
BASE
3. Distributed transaction scheme
Distributed transactions have the following solutions:
- XA
- TCC
- Seata framework AT transaction
- SAGA
- Reliable message final consistency
- Best effort notification
Order system
When a user places an order, the following three-step process is performed:
- The order system saves the order
- The order system calls the inventory service to reduce the commodity inventory
- The order system calls the account service to deduct the user's amount
These three steps should be managed as a whole, either successful or failed as a whole.
1. The order case involves four databases:
For the convenience of subsequent testing, we write a tool to reset all database tables, which can easily reset the data to the initial state.
2. Create the springboot subproject module
Add database dependent
3. Modify pom.xml file
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.drhj</groupId> <artifactId>db-init</artifactId> <version>0.0.1-SNAPSHOT</version> <name>db-init</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
4. Configure application.yml
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8 username: root password: root
5. Add sql script file
Next, create a new sql folder under the resources directory, and put four sql script files in the sql folder:
seata-server.sql
drop database if exists `seata`; CREATE DATABASE `seata` CHARSET utf8; use `seata`; -- -------------------------------- 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(96), `transaction_id` BIGINT, `branch_id` BIGINT NOT NULL, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_branch_id` (`branch_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8;
order.sql
drop database if exists `seata_order`; CREATE DATABASE `seata_order` charset utf8; use `seata_order`; CREATE TABLE `order` ( `id` bigint(11) NOT NULL, `user_id` bigint(11) DEFAULT NULL COMMENT 'user id', `product_id` bigint(11) DEFAULT NULL COMMENT 'product id', `count` int(11) DEFAULT NULL COMMENT 'quantity', `money` decimal(11,0) DEFAULT NULL COMMENT 'amount of money', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8; ALTER TABLE `order` ADD COLUMN `status` int(1) DEFAULT NULL COMMENT 'Order status: 0: being created; 1: Closed' AFTER `money` ; -- for AT mode you must to init this sql for you business database. the seata server not need it. CREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status', `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime', `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table'; CREATE TABLE IF NOT EXISTS segment ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 'Auto increment primary key', VERSION BIGINT DEFAULT 0 NOT NULL COMMENT 'Version number', business_type VARCHAR(63) DEFAULT '' NOT NULL COMMENT 'Business type, unique', max_id BIGINT DEFAULT 0 NOT NULL COMMENT 'Current maximum id', step INT DEFAULT 0 NULL COMMENT 'step', increment INT DEFAULT 1 NOT NULL COMMENT 'every time id increment', remainder INT DEFAULT 0 NOT NULL COMMENT 'remainder', created_at BIGINT UNSIGNED NOT NULL COMMENT 'Creation time', updated_at BIGINT UNSIGNED NOT NULL COMMENT 'Update time', CONSTRAINT uniq_business_type UNIQUE (business_type) ) CHARSET = utf8mb4 ENGINE INNODB COMMENT 'No. segment table'; INSERT INTO segment (VERSION, business_type, max_id, step, increment, remainder, created_at, updated_at) VALUES (1, 'order_business', 1000, 1000, 1, 0, NOW(), NOW());
storage.sql
drop database if exists `seata_storage`; CREATE DATABASE `seata_storage` charset utf8; use `seata_storage`; CREATE TABLE `storage` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `product_id` bigint(11) DEFAULT NULL COMMENT 'product id', `total` int(11) DEFAULT NULL COMMENT 'Total inventory', `used` int(11) DEFAULT NULL COMMENT 'Used inventory', `residue` int(11) DEFAULT NULL COMMENT 'Remaining inventory', `frozen` int(11) DEFAULT '0' COMMENT 'TCC Transaction locked inventory', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; INSERT INTO `seata_storage`.`storage` (`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0', '100'); -- for AT mode you must to init this sql for you business database. the seata server not need it. CREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status', `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime', `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
account.sql
drop database if exists `seata_account`; CREATE DATABASE `seata_account` charset utf8; use `seata_account`; CREATE TABLE `account` ( `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id', `user_id` bigint(11) UNIQUE DEFAULT NULL COMMENT 'user id', `total` decimal(10,0) DEFAULT NULL COMMENT 'Total amount', `used` decimal(10,0) DEFAULT NULL COMMENT 'Balance used', `residue` decimal(10,0) DEFAULT '0' COMMENT 'Remaining available limit', `frozen` decimal(10,0) DEFAULT '0' COMMENT 'TCC Amount of transaction lock', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; INSERT INTO `seata_account`.`account` (`id`, `user_id`, `total`, `used`, `residue`) VALUES ('1', '1', '1000', '0', '1000'); -- for AT mode you must to init this sql for you business database. the seata server not need it. CREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT(20) NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(100) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status', `log_created` DATETIME(6) NOT NULL COMMENT 'create datetime', `log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';
6. Add code to the main program and execute sql script
Spring provides a jdbc script executor. Using this tool, you can easily run an sql script file. Here is the method:
ScriptUtils.executeSqlScript()
Just pass in the parameters it needs.
The following code runs four script programs in the sql directory. Each run will delete four databases, recreate them, and initialize the data.
package com.drhj.dbinit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.EncodedResource; import org.springframework.jdbc.datasource.init.ScriptUtils; import javax.annotation.PostConstruct; import javax.sql.DataSource; @SpringBootApplication public class DbInitApplication { @Autowired private DataSource dataSource; public static void main(String[] args) { SpringApplication.run(DbInitApplication.class, args); } @PostConstruct public void init() throws Exception{ exec("sql/account.sql"); exec("sql/order.sql"); exec("sql/seata-server.sql"); exec("sql/storage.sql"); } private void exec (String sql) throws Exception { ClassPathResource cpr = new ClassPathResource(sql, DbInitApplication.class.getClassLoader()); EncodedResource resource = new EncodedResource(cpr, "UTF-8"); //spring jdbc provides a tool to execute sql script files ScriptUtils.executeSqlScript(dataSource.getConnection(), resource); } }
The project is started and the database is refreshed successfully
1. Create a new springboot Module: Eureka server
Add dependency
2. Modify 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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.drhj</groupId> <artifactId>eureka</artifactId> <version>0.0.1-SNAPSHOT</version> <name>eureka</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <spring-cloud.version>Hoxton.SR12</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>$</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
3. Configure application.yml
spring: application: name: eureka-server server: port: 8761 eureka: server: enable-self-preservation: false client: register-with-eureka: false fetch-registry: false
4. Add @ EnableEurekaServer to the startup class
package com.drhj.eureka; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @EnableEurekaServer @SpringBootApplication public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); } }
5. Test
Start project
1. Create Maven's module
2. Modify 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"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.drhj</groupId> <artifactId>order-parent</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <name>order-parent</name> <modules> <module>account</module> <module>storage</module> <module>order</module> </modules> <properties> <mybatis-plus.version>3.3.2</mybatis-plus.version> <druid-spring-boot-starter.version>1.1.23</druid-spring-boot-starter.version> <seata.version>1.3.0</seata.version> <spring-cloud-alibaba-seata.version>2.0.0.RELEASE</spring-cloud-alibaba-seata.version> <spring-cloud.version>Hoxton.SR12</spring-cloud.version> </properties> <dependencies> <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-openfeign</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>$</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>$</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>$</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>6, account item
1. Create subprojects accout, springboot and module under order parent
Attention path
2. Modify pom.xml
Because the parent project has added the required dependencies, there is no need to add dependencies here. You can specify the parent project
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>order-parent</artifactId> <groupId>com.drhj</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>account</artifactId> <version>0.0.1-SNAPSHOT</version> <name>account</name> </project>
3. Configure application.yml
spring: application: name: account datasource: url: jdbc:mysql:///seata_account?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8 driver-class-name: com.mysql.cj.jdbc.Driver username: root password: root # account 8081 storage 8082 order 8083 server: port: 8081 eureka: client: service-url: defaultZone: http://localhost:8761/eureka instance: prefer-ip-address: true mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.drhj.account.entity configuration: map-underscore-to-camel-case: true #Hump naming logging: #Print log level: com.drhj.account.mapper: debug
4. Configure bootstrap.yml and specify the network segment
spring: cloud: inetutils: preferred-networks: - 10\.1\.6\..+ - 192\.168\.0\..+
5. Add entity class
package com.drhj.account.entity; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.math.BigDecimal; @Data @NoArgsConstructor @AllArgsConstructor public class Account { private Long id; private Long userId; //User id private BigDecimal total; //total private BigDecimal used; //Used, consumed amount private BigDecimal residue; //Available amount private BigDecimal frozen; //Frozen amount }
6. Create AccountMapper interface
package com.drhj.account.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.drhj.account.entity.Account; import org.apache.ibatis.annotations.Mapper; import java.math.BigDecimal; @Mapper public interface AccountMapper extends BaseMapper<Account> { //Deduct account amount void decrease(Long userId, BigDecimal money); }
7.Mapper configuration
First create a new folder mapper in the resources directory, and then create the file AccountMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.drhj.account.mapper.AccountMapper" > <resultMap id="BaseResultMap" type="Account" > <id column="id" property="id" jdbcType="BIGINT" /> <result column="user_id" property="userId" jdbcType="BIGINT" /> <result column="total" property="total" jdbcType="DECIMAL" /> <result column="used" property="used" jdbcType="DECIMAL" /> <result column="residue" property="residue" jdbcType="DECIMAL"/> <result column="frozen" property="frozen" jdbcType="DECIMAL"/> </resultMap> <update id="decrease"> UPDATE account SET residue = residue - #,used = used + # where user_id = #; </update> </mapper>
8. Add Mybatis scanning annotation to the main program
Add annotation @ MapperScan("com.drhj.account.mapper"):
package com.drhj.account; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @MapperScan("com.drhj.account.mapper") @SpringBootApplication public class AccountApplication { public static void main(String[] args) { SpringApplication.run(AccountApplication.class, args); } }
9. Add AccountService interface and its implementation class
The decrease() method realizes the function of deducting the account amount
package com.drhj.account.service; import java.math.BigDecimal; public interface AccountService { //Deduction account void decrease(Long userId, BigDecimal money); }
package com.drhj.account.service; import com.drhj.account.mapper.AccountMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.math.BigDecimal; @Service public class AccountServiceImpl implements AccountService{ @Autowired private AccountMapper accountMapper; @Override public void decrease(Long userId, BigDecimal money) { accountMapper.decrease(userId,money); } }
10. Add AccountController class to provide client access interface
package com.drhj.account.controller; import com.drhj.account.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; @RestController public class AccountController { @Autowired private AccountService accountService; @GetMapping("/decrease") //?userId=x&money=y public String decrease(Long userId, BigDecimal money) { accountService.decrease(userId,money); return "Deduction account succeeded"; } }
11. Test
1) View eureka registration information
visit http://localhost:8761/ To view the registration information of the account service in eureka:
2) Access account services to perform account deductions
visit http://localhost:8081/decrease?userId=1&money=100
3) View the sql log executed by the console Mybatis
4) Check the database table to confirm that the amount has been deducted
The storage inventory micro service item is used to reduce inventory.
1. Create a new springboot Module: storage
2. Modify 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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>order-parent</artifactId> <groupId>com.drhj</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>storage</artifactId> <version>0.0.1-SNAPSHOT</version> <name>storage</name> </project>
3. Configure application.yml
spring: application: name: storage datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql:///seata_storage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8 username: root password: root # account 8081 storage 8082 order 8083 server: port: 8082 eureka: client: service-url: defaultZone: http://localhost:8761/eureka instance: prefer-ip-address: true mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.drhj.storage.entity configuration: map-underscore-to-camel-case: true #Hump naming logging: #Print log level: com.drhj.storage.mapper: debug
4. Configure bootstrap.yml
spring: cloud: inetutils: preferred-networks: - 10\.1\.6\..+ - 192\.168\.0\..+
5. Create entity class
package com.drhj.storage.entity; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class Storage { private Long id; private Long productId; //Commodity id private Integer total; //total private Integer used; //Used, sold private Integer residue; //Available stock private Integer frozen; //Frozen inventory }
6. Create StorageMapper interface
package com.drhj.storage.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.drhj.storage.entity.Storage; import org.apache.ibatis.annotations.Mapper; @Mapper public interface StorageMapper extends BaseMapper<Storage> { //reduce stock void decrease(Long productId,Integer count); }
7.Mapper configuration
First create a new folder mapper in the resources directory, and then create the file StorageMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.drhj.storage.mapper.StorageMapper" > <resultMap id="BaseResultMap" type="Storage" > <id column="id" property="id" jdbcType="BIGINT" /> <result column="product_id" property="productId" jdbcType="BIGINT" /> <result column="total" property="total" jdbcType="INTEGER" /> <result column="used" property="used" jdbcType="INTEGER" /> <result column="residue" property="residue" jdbcType="INTEGER" /> </resultMap> <update id="decrease"> UPDATE storage SET used = used + #,residue = residue - # WHERE product_id = # </update> </mapper>
8. Add Mybatis scanning annotation to the main program
Add annotation @ MapperScan("com.drhj.storage.mapper"):
package com.drhj.storage; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @MapperScan("com.drhj.storage.mapper") @SpringBootApplication public class StorageApplication { public static void main(String[] args) { SpringApplication.run(StorageApplication.class, args); } }
9. Add StorageService interface and its implementation class
The decrease() method implements the function of reducing commodity inventory.
package com.drhj.storage.service; public interface StorageService { //reduce stock void decrease(Long productId,Integer count); }
package com.drhj.storage.service; import com.drhj.storage.mapper.StorageMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class StorageServiceImpl implements StorageService { @Autowired private StorageMapper storageMapper; @Override public void decrease(Long productId, Integer count) { storageMapper.decrease(productId,count); } }
10. Add StorageController class to provide client access interface
package com.drhj.storage.controller; import com.drhj.storage.service.StorageService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class StorageController { @Autowired private StorageService storageService; @GetMapping("/decrease") //?productId=1&count=1 public String decrease(Long productId,Integer count) { storageService.decrease(productId,count); return "Inventory reduction successful"; } }
11. Test
1) View eureka registration information
visit http://localhost:8761/ To view the registration information of inventory service in eureka:
2) Access inventory services and perform inventory reduction operations
visit http://localhost:8082/decrease?productId=1&count=1
3) Check the database table to confirm that the amount has been deducted
The order item saves the order and calls storage and account to reduce inventory and deduct amount.
1. Create a new springboot Module: order
2. Modify 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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>order-parent</artifactId> <groupId>com.drhj</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>order</artifactId> <version>0.0.1-SNAPSHOT</version> <name>order</name> </project>
3. Configure application.yml
spring: application: name: order datasource: url: jdbc:mysql:///seata_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8 driver-class-name: com.mysql.cj.jdbc.Driver username: root password: root # account 8081 storage 8082 order 8083 server: port: 8083 eureka: client: service-url: defaultZone: http://localhost:8761/eureka instance: prefer-ip-address: true mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.drhj.order.entity configuration: map-underscore-to-camel-case: true #Hump naming logging: #Print log level: com.drhj.order.mapper: debug
4. Configure bootstrap.yml
spring: cloud: inetutils: preferred-networks: - 10\.1\.6\..+ - 192\.168\.0\..+
5. Create order entity class
package com.drhj.order.entity; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.math.BigDecimal; @Data @NoArgsConstructor @AllArgsConstructor public class Order { private Long id; private Long userId; //User id private Long productId; //Commodity id private Integer count; //Quantity purchased private BigDecimal money; //How much does it cost private Integer status; //Status, 0-frozen, 1-normal }
6. Create OrderMapper interface
package com.drhj.order.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.drhj.order.entity.Order; public interface OrderMapper extends BaseMapper<Order> { //Create order void create(Order order); }
7.Mapper configuration
First create a new folder mapper in the resources directory, and then create the file OrderMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.drhj.order.mapper.OrderMapper" > <resultMap id="BaseResultMap" type="Order" > <id column="id" property="id" jdbcType="BIGINT" /> <result column="user_id" property="userId" jdbcType="BIGINT" /> <result column="product_id" property="productId" jdbcType="BIGINT" /> <result column="count" property="count" jdbcType="INTEGER" /> <result column="money" property="money" jdbcType="DECIMAL" /> <result column="status" property="status" jdbcType="INTEGER" /> </resultMap> <insert id="create"> INSERT INTO `order` (`id`,`user_id`,`product_id`,`count`,`money`,`status`) VALUES(#, #, #, #, #,1); </insert> </mapper>
8. Add Mybatis scanning annotation to the main program
Add annotation @ MapperScan("com.drhj.order.mapper"):
package com.drhj.order; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @MapperScan("com.drhj.order.mapper") @SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } }
9. Add the OrderService interface and its implementation class
The create() method implements the function of saving orders.
package com.drhj.order.service; import com.drhj.order.entity.Order; public interface OrderService { //Create order void create(Order order); }
package com.drhj.order.service; import com.drhj.order.entity.Order; import com.drhj.order.mapper.OrderMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Random; @Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Override public void create(Order order) { //Call the issuer remotely to generate the order id //First, the id is generated temporarily and randomly. After adding the sender, this line of code is deleted Long id = Math.abs(new Random().nextLong()); order.setId(id); orderMapper.create(order); //Remote call inventory to reduce inventory //Remote call account, deduction account } }
10. Add the OrderController class to provide the client access interface
package com.drhj.order.controller; import com.drhj.order.entity.Order; import com.drhj.order.service.OrderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class OrderController { @Autowired private OrderService orderService; @GetMapping("/create") public String create(Order order) { orderService.create(order); return "Order created successfully"; } }
11. Test
1) View eureka registration information
visit http://localhost:8761/ To view the registration letter of order service in eureka:
2) Access the order service and execute order saving
visit http://localhost:8083/create?userId=1&productId=1&count=10&money=100
3) View the sql log executed by the console Mybatis
4) Check the database table and confirm that the order is saved successfully
In a distributed system, the service system that generates a unique serial number is commonly known as the sender.
There are many open source projects of signal generator. EasyIdGenerator is used here. Please visit for specific project information
https://github.com/lookingatstarts/easyIdGenerator
1. Download project
visit https://github.com/lookingatstarts/easyIdGenerator , download the signer project.
Unzip it into our parent project directory and modify the name
2. Import module
1) Drag the pom.xml file directly into the idea
2) Modify pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" 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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.2.RELEASE</version> <relativePath/> </parent> <groupId>com.easy.id</groupId> <artifactId>easy-id-generator</artifactId> <version>1.0-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.source>1.8</maven.compiler.source> <junit.version>4.12</junit.version> <mysql.connector.version>8.0.16</mysql.connector.version> <com.alibaba.fastjson.version>1.2.62</com.alibaba.fastjson.version> <lombok.version>1.18.8</lombok.version> <curator.version>2.6.0</curator.version> </properties> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>$</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>$</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>$</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR12</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> </project>
3) Right click -- > Add as Maven project
3. Configure application.yml
Because we created the database ourselves, we don't use his database
server: port: 9090 easy-id-generator: snowflake: #Snowflake algorithm enable: false #close zk: connection-string: 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183 load-worker-id-from-file-when-zk-down: true # When zk is inaccessible, the previously backed up workerId is read from the local file segment: #Generate id using database enable: true db-list: "seata_order" #["db1","db2"] fetch-segment-retry-times: 3 # Failed to get segment number from database retries spring: application: name: easy-id eureka: client: service-url: defaultZone: http://localhost:8761/eureka instance: prefer-ip-address: true
4. Configure seata_order.properties
jdbcUrl=jdbc:mysql:///seata_order?serverTimezone=GMT%2B8&autoReconnect=true&useUnicode=true&characterEncoding=UTF-8 driverClassName=com.mysql.cj.jdbc.Driver dataSource.user=root dataSource.password=root dataSource.cachePrepStmts=true dataSource.prepStmtCacheSize=250 dataSource.prepStmtCacheSqlLimit=2048
5. Configure bootstrap.yml
spring: cloud: inetutils: preferred-networks: - 10\.1\.6\..+ - 192\.168\.0\..+
7. View the database control layer method it uses
package com.easy.id.web; import com.easy.id.config.Module; import com.easy.id.service.EasyIdService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.validation.constraints.NotEmpty; import java.util.Set; import java.util.stream.Collectors; /** * @author zhangbingbing * @version 1.0.0 * @createTime 2020 May 29 */ @RestController @RequestMapping("/segment/ids") @Validated @Module(value = "segment.enable") public class SegmentEasyIdController { //http://localhost:9090/segment/ids/next_id?businessType=order_business @Autowired @Qualifier("segmentEasyIdService") private EasyIdService easyIdService; @GetMapping("/next_id") public String getNextId(@NotEmpty String businessType) { return easyIdService.getNextId(businessType).toString(); } @GetMapping("/next_id/batches") public Set<String> getNextId(@RequestParam(value = "batches_size", defaultValue = "100") Integer batchSize, @NotEmpty String businessType) { return easyIdService.getNextIdBatch(businessType, batchSize).stream() .map(Object::toString).collect(Collectors.toSet()); } }
8. Test
1) Start service
2) View registration information in eureka
3) According to the settings of the SegmentEasyIdController class, access the following address to obtain the auto increment id:
http://localhost:9090/segment/ids/next_id?businessType=order_business
Google generally can't see it. You can right-click -- > to view it
Or use another browser
1. Business analysis
When an order module is generated, the system first gives it a globally unique id. accordingly, the funds corresponding to the personal account will be reduced and the commodity inventory will be reduced;
Therefore, it is necessary to remotely call the easy id generator, account module and storage module;
2. Add dependency
Since the order parent in the parent project has downloaded the corresponding dependencies, there is no need to add them here.
3. Add notes to startup items
Because the order project calls other projects, add @ EnableFeignClients on the startup item of order
package com.drhj.order; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; @EnableFeignClients @MapperScan("com.drhj.order.mapper") @SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } }
4. Add Feign declarative client interface
Sender client interface:
package com.drhj.order.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; @FeignClient(name = "easy-id") //It corresponds to the name in the eureka registry and is case insensitive public interface EasyIdClient { @GetMapping("/segment/ids/next_id") String nextId(@RequestParam("businessType") String businessType); }
Client interface of account service:
package com.drhj.order.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import java.math.BigDecimal; @FeignClient(name = "account") public interface AccountClient { @GetMapping("/decrease") String decrease(@RequestParam("userId") Long useId, @RequestParam("money") BigDecimal money); }
Client interface of inventory service:
package com.drhj.order.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; @FeignClient(name = "storage") public interface StorageClient { @GetMapping("/decrease") String decrease(@RequestParam("productId") Long productId,@RequestParam("count") Integer count); }
5. Call the remote service through Feign client in order business code
package com.drhj.order.service; import com.drhj.order.entity.Order; import com.drhj.order.feign.AccountClient; import com.drhj.order.feign.EasyIdClient; import com.drhj.order.feign.StorageClient; import com.drhj.order.mapper.OrderMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private EasyIdClient easyIdClient; @Autowired private AccountClient accountClient; @Autowired private StorageClient storageClient; @Override public void create(Order order) { //Call the issuer remotely to generate the order id String s = easyIdClient.nextId("order_business"); Long id = Long.valueOf(s); order.setId(id); orderMapper.create(order); //Remote call inventory to reduce inventory storageClient.decrease(order.getProductId(),order.getCount()); //Remote call account, deduction account accountClient.decrease(order.getUserId(),order.getMoney()); } }
6. Test
Start all items and access the order items for testing:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
1) View execution results
2) View console log
order
account
storage
3) View data changes in three databases
order
account
storage
So far, the project has been built.