[distributed transaction] 01--Spring Cloud project construction

1, Basic introduction

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

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

2, Business scenario

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.

3, Create parent project


3, Database initialization tool

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

4, eureka registry

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>${spring-cloud.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

5, Create parent project of microservice

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>${mybatis-plus.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>${druid-spring-boot-starter.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>${spring-cloud.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 - #{money},used = used + #{money} where user_id = #{userId};
    </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

7, storage inventory item

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 + #{count},residue = residue - #{count} WHERE product_id = #{productId}
    </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

8, order item

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(#{id}, #{userId}, #{productId}, #{count}, #{money},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

9, Globally unique id sender

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>${mysql.connector.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>${com.alibaba.fastjson.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>${curator.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

10, Remote call configuration

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

11, Backup


So far, the project has been built.

Tags: Java Distribution Spring Cloud

Posted on Wed, 27 Oct 2021 09:39:01 -0400 by ArcAiN6