分布式事務四-Seata AT模式-案例


分布式事務(四)Seata AT模式-Spring Cloud微服務案例

訂單業務案例

創建 Empty Project:seata-at

數據庫初始化工具

訂單案例涉及四個數據庫:

a

為了后續測試方便我們編寫一個工具,用來重置所有數據庫表,可以方便地把數據重置到初始狀態。

新建Module:db-init

新建 Module,選擇 Spring Initializr

a

填寫 Group 和 Artifact,其他選項默認即可:
a

添加 JDBCMySQL Driver 依賴:

a
完成后,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.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.tedu</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-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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.yml 配置

項目的 application.properties 文件改名成 application.yml,然后添加 mysql 連接配置:

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost/?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    username: root
    password: 1234

注意: 在連接地址中沒有指定庫名,是因為我們要先后連接四個數據庫,后面執行的 sql 腳本文件中會執行 use 來進行數據庫切換。

添加 sql 腳本文件

案例中的 sql 腳本來自不同的項目:

下面,在 resources 目錄下,先新建一個 sql 文件夾,四個 sql 腳本文件放在 sql 文件夾下:

sql

  • 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 '用戶id',
  `product_id` bigint(11) DEFAULT NULL COMMENT '產品id',
  `count` int(11) DEFAULT NULL COMMENT '數量',
  `money` decimal(11,0) DEFAULT NULL COMMENT '金額',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

ALTER TABLE `order` ADD COLUMN `status` int(1) DEFAULT NULL COMMENT '訂單狀態:0:創建中;1:已完結' 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 '自增主鍵',
    VERSION       BIGINT      DEFAULT 0  NOT NULL COMMENT '版本號',
    business_type VARCHAR(63) DEFAULT '' NOT NULL COMMENT '業務類型,唯一',
    max_id        BIGINT      DEFAULT 0  NOT NULL COMMENT '當前最大id',
    step          INT         DEFAULT 0  NULL COMMENT '步長',
    increment     INT         DEFAULT 1  NOT NULL COMMENT '每次id增量',
    remainder     INT         DEFAULT 0  NOT NULL COMMENT '余數',
    created_at    BIGINT UNSIGNED        NOT NULL COMMENT '創建時間',
    updated_at    BIGINT UNSIGNED        NOT NULL COMMENT '更新時間',
    CONSTRAINT uniq_business_type UNIQUE (business_type)
) CHARSET = utf8mb4
  ENGINE INNODB COMMENT '號段表';


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 '產品id',
  `total` int(11) DEFAULT NULL COMMENT '總庫存',
  `used` int(11) DEFAULT NULL COMMENT '已用庫存',
  `residue` int(11) DEFAULT NULL COMMENT '剩余庫存',
  `frozen` int(11) DEFAULT '0' COMMENT 'TCC事務鎖定的庫存',
  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 '用戶id',
  `total` decimal(10,0) DEFAULT NULL COMMENT '總額度',
  `used` decimal(10,0) DEFAULT NULL COMMENT '已用余額',
  `residue` decimal(10,0) DEFAULT '0' COMMENT '剩余可用額度',
  `frozen` decimal(10,0) DEFAULT '0' COMMENT 'TCC事務鎖定的金額',
  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';

主程序中添加代碼,執行sql腳本

Spring 中提供了一個 jdbc 腳本執行器,使用這個工具可以非常方便的運行一個 sql 腳本文件,下面是這個方法:

ScriptUtils.executeSqlScript()

只需要傳入它需要的參數即可。

下面代碼運行 sql 目錄中的四個腳本程序,每次運行都會刪除四個數據庫再重新創建,並初始化數據。

package cn.tedu.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;
import java.sql.SQLException;

@SpringBootApplication
public class DbInitApplication {

    @Autowired
    private DataSource dataSource;

    public static void main(String[] args) {
        SpringApplication.run(DbInitApplication.class, args);
    }

    @PostConstruct //spring boot 初始化完成以后,會自動運行此方法
    public void  init() throws SQLException {
        exec("sql/account.sql");
        exec("sql/order.sql");
        exec("sql/seata-server.sql");
        exec("sql/storage.sql");
    }

    private void exec(String s) throws SQLException {
        ClassPathResource resource = new ClassPathResource(s, DbInitApplication.class.getClassLoader());
        EncodedResource encodedResource = new EncodedResource(resource, "UTF-8");
        ScriptUtils.executeSqlScript(dataSource.getConnection(),encodedResource);

    }
}

eureka注冊中心

新建 Module:eureka-server

新建 Module:

a

設置 Group 和 Artifact,其他默認:

a
選擇 eureka server 依賴:
a

設置項目名 eureka-server,存放路徑放在 seata-at 目錄下:

a

application.yml 配置

spring:
  application:
    name: eureka-server

server:
  port: 8761

eureka:
  server:
    enable-self-preservation: false
  instance:
    hostname: eureka1
  client:
    register-with-eureka: false
    fetch-registry: false

主程序添加 @EnableEurekaServer 注解

package cn.tedu.eurekaserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }

}

啟動服務可以檢查eureka是否正常

POM工程搭建

父項目

為了對 order、storage和account微服務項目依賴進行統一管理,這里創建一個 pom 類型的 maven 項目,作為父項目。

新建 Maven 項目:order-parent

新建 Module,選擇 Maven 項目:

a
填寫設置,將 order-parent 項目存放在 seata-at 項目目錄下:
a

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.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.tedu</groupId>
    <artifactId>order-parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <name>order-parent</name>
    <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.SR8</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>com.alibaba.cloud</groupId>
          <artifactId>spring-cloud-alibaba-seata</artifactId>
          <version>${spring-cloud-alibaba-seata.version}</version>
          <exclusions>
            <exclusion>
              <artifactId>seata-all</artifactId>
              <groupId>io.seata</groupId>
            </exclusion>
          </exclusions>
        </dependency>-->
        <dependency>
          <groupId>io.seata</groupId>
          <artifactId>seata-all</artifactId>
          <version>${seata.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>

account賬戶項目

在這個微服務項目中實現扣減賬戶金額的功能。

新建 Module:account

新建Module:
a
設置 Group 和 Artifact,其它選項默認:

a
不選擇任何依賴,直接點下一步,這個項目要繼承 order-parent 項目:

a
設置 Module 名稱和存儲目錄:

a

修改 pom.xml

修改 pom.xml,設置繼承父項目 order-parent:

<?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>cn.tedu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.tedu</groupId>
    <artifactId>account</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>account</name>
    <description>Demo project for Spring Boot</description>



</project>

application.yml 配置

spring:
  application:
    name: account
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost/seata_account?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    username: root
    password: 1234

server:
  port: 8081

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    prefer-ip-address: true

mybatis-plus:
  type-aliases-package: cn.tedu.account.entity
  mapper-locations:
    - classpath:mapper/*Mapper.xml
  configuration:
    map-underscore-to-camel-case: true

logging:
  level:
    cn.tedu.account.mapper: DEBUG

Mapper配置

先在 resources 目錄下新建文件夾 mapper,然后創建文件 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="cn.tedu.account.mapper.AccountMapper" >
    <resultMap id="BaseResultMap" type="cn.tedu.account.entity.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>

創建 Account 實體類

package cn.tedu.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;
    private BigDecimal total;
    private BigDecimal used;
    private BigDecimal residue;
    private BigDecimal frozen;
}

創建 AccountMapper 類

這里繼承 Mybatis-Plus 提供的通用 Mapper 父類

package cn.tedu.account.mapper;

import cn.tedu.account.entity.Account;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import java.math.BigDecimal;

public interface AccountMapper extends BaseMapper<Account> {
    void decrease(Long userId, BigDecimal money);
}

添加 AccountService 接口和它的實現類

decrease() 方法實現扣減賬戶金額的功能

package cn.tedu.account.service;

import java.math.BigDecimal;

public interface AccountService {
    void decrease(Long userId, BigDecimal money);
}
1234567
package cn.tedu.account.service;

import cn.tedu.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);
    }
}

添加 AccountController 類提供客戶端訪問接口

package cn.tedu.account.controller;

import cn.tedu.account.service.AccountService;
import lombok.extern.slf4j.Slf4j;
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
@Slf4j
public class AccountController {
    @Autowired
    private AccountService accountService;

    @GetMapping("/decrease")
    public String decrease(Long userId, BigDecimal money) {
        accountService.decrease(userId,money);
        return "用戶賬戶扣減金額成功";
    }
}

主程序添加 Mybatis 掃描注解

添加注解 @MapperScan("cn.tedu.account.mapper")

package cn.tedu.account;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("cn.tedu.account.mapper")
@SpringBootApplication
public class AccountApplication {

    public static void main(String[] args) {
        SpringApplication.run(AccountApplication.class, args);
    }

}

啟動 account 項目進行測試

  1. 查看 eureka 注冊信息
    訪問 http://localhost:8761/ 查看賬戶服務在 eureka 中的注冊信息:

a

  1. 訪問賬戶服務執行賬戶扣減金額
    訪問 http://localhost:8081/decrease?userId=1&money=100

a

  1. 查看控制台 Mybatis 執行的 sql 日志

a

  1. 查看數據庫表,確認金額已經被減掉

a

storage庫存項目

storage 庫存微服務項目,用來實現減少庫存的功能。

新建 Module:storage

新建Module:
a
設置 Group 和 Artifact,其它選項默認:
a

不選擇任何依賴,直接點下一步,這個項目要繼承 order-parent 項目:

a

設置 Module 名稱和存儲目錄:

a

修改 pom.xml

修改 pom.xml,設置繼承父項目 order-parent:

<?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>cn.tedu</groupId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../order-parent/pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.tedu</groupId>
    <artifactId>storage</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>storage</name>
    <description>Demo project for Spring Boot</description>


</project>

application.yml 配置

spring:
  application:
    name: storage
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost/seata_storage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    username: root
    password: 1234

server:
  port: 8082

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    prefer-ip-address: true

mybatis-plus:
  type-aliases-package: cn.tedu.storage.entity
  mapper-locations:
    - classpath:/mapper/*Mapper.xml
  configuration:
    map-underscore-to-camel-case: true

logging:
  level:
    cn.tedu.storage.mapper: DEBUG

Mapper文件配置

先在 resources 目錄下新建文件夾 mapper,然后創建文件 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="cn.tedu.storage.mapper.StorageMapper" >
    <resultMap id="BaseResultMap" type="cn.tedu.storage.entity.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>

創建 storage 實體類

package cn.tedu.storage.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Storage {
    private Long id;
    private Long productId;
    private Integer total;
    private Integer used;
    private Integer residue;
    private Integer frozen;
}

創建 StorageMapper 類

這里繼承 Mybatis-Plus 提供的通用 Mapper 父類

package cn.tedu.storage.mapper;

import cn.tedu.storage.entity.Storage;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

public interface StorageMapper extends BaseMapper<Storage> {
    void decrease(Long productId, Integer count);
}

添加 StorageService 接口和它的實現類

decrease() 方法實現減少商品庫存功能。

package cn.tedu.storage.service;

public interface StorageService {
    void decrease(Long productId, Integer count) throws Exception;
}
12345
package cn.tedu.storage.service;

import cn.tedu.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) throws Exception {
        storageMapper.decrease(productId,count);
    }
}

添加 StorageController 類

提供客戶端訪問接口

package cn.tedu.storage.controller;

import cn.tedu.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")
    public String decrease(Long productId, Integer count) throws Exception {
        storageService.decrease(productId,count);
        return "減少商品庫存成功";
    }

}

主程序添加 Mybatis 掃描注解

添加注解 @MapperScan("cn.tedu.storage.mapper")

package cn.tedu.storage;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("cn.tedu.storage.mapper")
@SpringBootApplication
public class StorageApplication {

    public static void main(String[] args) {
        SpringApplication.run(StorageApplication.class, args);
    }

}

啟動 storage 項目進行測試

  1. 查看 eureka 注冊信息
    訪問 http://localhost:8761/ 查看庫存服務在 eureka 中的注冊信息:

a

  1. 訪問庫存服務,執行減少庫存操作
    訪問 http://localhost:8082/decrease?productId=1&count=1

    提示減少商品庫存成功

a

  1. 查看控制台 Mybatis 執行的 sql 日志

a

  1. 查看數據庫表,確認金額已經被減掉

a

order訂單項目

order 訂單項目保存訂單,並調用 storage 和 account 減少庫存和扣減金額。

需要生成全局唯一id

新建 Module:order

新建Module:
a

設置 Group 和 Artifact,其它選項默認:

a
不選擇任何依賴,直接點下一步,這個項目要繼承 order-parent 項目:

a

設置 Module 名稱和存儲目錄:

a

修改 pom.xml

修改 pom.xml,設置繼承父項目 order-parent:

<?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>cn.tedu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.tedu</groupId>
    <artifactId>order</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>order</name>
    <description>Demo project for Spring Boot</description>

</project>

application.yml 配置

spring:
  application:
    name: order

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost/seata_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    username: root
    password: 1234

server:
  port: 8083

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    prefer-ip-address: true

mybatis-plus:
  type-aliases-package: cn.tedu.sdorder.entity
  mapper-locations:
    - classpath:/mapper/*Mapper.xml
  configuration:
    map-underscore-to-camel-case: true

logging:
  level:
    cn.tedu.order.mapper: DEBUG

Mapper文件配置

先在 resources 目錄下新建文件夾 mapper,然后創建文件 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="cn.tedu.order.mapper.OrderMapper" >
    <resultMap id="BaseResultMap" type="cn.tedu.order.entity.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>

創建 order 實體類

package cn.tedu.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;
    private Long productId;
    private Integer count;
    private BigDecimal money;
    private Integer status;
}

創建 OrderMapper 類

這里繼承 Mybatis-Plus 提供的通用 Mapper 父類

package cn.tedu.order.mapper;

import cn.tedu.order.entity.Order;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;


public interface OrderMapper extends BaseMapper<Order> {
    void create(Order order);
}

添加 OrderService 接口和它的實現類

create() 方法實現保存訂單的功能。

package cn.tedu.order.service;

import cn.tedu.order.entity.Order;

public interface OrderService {
    void create(Order order);
}

這里添加了三個 // TODO 注釋,之后通過feign以及全局的id發號器完成

package cn.tedu.order.service;

import cn.tedu.order.entity.Order;
import cn.tedu.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) {
        // TODO: 從全局唯一id發號器獲得id,這里暫時隨機產生一個 orderId
        Long orderId = Long.valueOf(new Random().nextInt(Integer.MAX_VALUE));
        order.setId(orderId);

        orderMapper.create(order);

        // TODO: 調用storage,修改庫存

        // TODO: 調用account,修改賬戶余額

    }
}

添加 OrderController 類

package cn.tedu.order.controller;

import cn.tedu.order.entity.Order;
import cn.tedu.order.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class OrderController {
    @Autowired
    OrderService orderService;
    /*
    用戶用這個路徑進行訪問:
    http://localhost:8083/create?userId=1&productId=1&count=10&money=100
     */
    @GetMapping("/create")
    public String create(Order order) {
        log.info("創建訂單");
        orderService.create(order);
        return "創建訂單成功";
    }
}

主程序添加 Mybatis 掃描注解

添加注解 @MapperScan("cn.tedu.order.mapper")

package cn.tedu.order;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("cn.tedu.order.mapper")
@SpringBootApplication
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

}

啟動 order 項目進行測試

  1. 查看 eureka 注冊信息
    訪問 http://localhost:8761/ 查看訂單服務在 eureka 中的注冊信:

a

  1. 訪問訂單服務,執行訂單保存
    訪問 http://localhost:8083/create?userId=1&productId=1&count=10&money=100

    提示創建訂單成功。

a

  1. 查看控制台 Mybatis 執行的 sql 日志

a

  1. 查看數據庫表,確認訂單保存成功

a

全局唯一id發號器

分布式系統中,產生唯一流水號的服務系統俗稱發號器。

有很多發號器開源項目,這里使用 EasyIdGenerator,具體項目信息請訪問:https://github.com/lookingatstarts/easyIdGenerator

下載項目

訪問 https://github.com/lookingatstarts/easyIdGenerator ,下載發號器項目。

然后解壓到當前項目的目錄中

修改目錄名稱為easy-id-generator

導入 Module

在Maven工具窗口中點擊添加按鈕,選擇發號器項目的 pom.xml 文件導入該項目:

注意: 如果右側沒有Maven工具標簽,請按兩下shift鍵,然后查找 “add maven projects” 就可以找到這個工具。

a

添加配置

pom.xml

發號器向 eureka 進行注冊,以便其它服務發現它。

在pom.xml 中添加 Spring Cloud Eureka Client 依賴:

<?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.1.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>
            <version>${lombok.version}</version>
            <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>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>Greenwich.SR6</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

application.yml

  1. 配置使用數據庫來生成自增id
  2. 向eureka進行注冊
server:
  port: 9090

easy-id-generator:
  snowflake:
    enable: false # 表示關閉雪花算法
    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  # 當zk不可訪問時,從本地文件中讀取之前備份的workerId
  segment:
    enable: true # true 表示啟用 數據庫算法
    db-list: seata_order
    fetch-segment-retry-times: 3 # 從數據庫獲取號段失敗重試次數

spring:
  application:
    name: easy-id-generator
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka

seata_order.properties 數據庫配置

在 resources 目錄下新建配置文件 seata_order.properties,配置 seata_order 數據庫的連接信息。

jdbcUrl=jdbc:mysql://localhost:3306/seata_order?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
driverClassName=com.mysql.cj.jdbc.Driver
dataSource.user=root
dataSource.password=1234
dataSource.cachePrepStmts=true
dataSource.prepStmtCacheSize=250
dataSource.prepStmtCacheSqlLimit=2048

db-init 項目的 order.sql 中已經創建了數據表,並插入了一個名為 order_business 的自增id條目。

項目的 schema.sql 中為示例數據表。

啟動項目並訪問測試

  1. 查看 eureka 中的注冊信息

a

  1. 訪問測試

根據 SegmentEasyIdController 類的設置,訪問下面地址獲取自增id:
http://localhost:9090/segment/ids/next_id?businessType=order_business

a

order訂單添加Feign,調用庫存和賬戶服務

  • 調用發號器獲得全局唯一id
  • 調用庫存服務減少商品庫存
  • 調用賬戶服務扣減用戶金額

application.yml

ribbon 默認超時時間是1秒,為了方便分布式事務測試,把超時時長改為 10 秒:

spring:
  application:
    name: order

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost/seata_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    username: root
    password: root

server:
  port: 8083

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    prefer-ip-address: true

mybatis-plus:
  type-aliases-package: cn.tedu.sdorder.entity
  mapper-locations:
    - classpath:/mapper/*Mapper.xml
  configuration:
    map-underscore-to-camel-case: true

logging:
  level:
    cn.tedu.order.mapper: DEBUG
    
ribbon:
  ReadTimeout: 10000

主程序添加注解啟用 Feign

添加 @EnableFeignClients 注解:

package cn.tedu.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("cn.tedu.order.mapper")
@SpringBootApplication
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

}

添加Feign聲明式客戶端接口

發號器的客戶端接口:

package cn.tedu.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-GENERATOR")
public interface EasyIdGeneratorClient {
    @GetMapping("/segment/ids/next_id")
    Long nextId(@RequestParam String businessType);
}

庫存服務的客戶端接口:

package cn.tedu.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 Long productId, @RequestParam Integer count);
}

賬戶服務的客戶端接口:

package cn.tedu.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 Long userId, @RequestParam BigDecimal money);
}

在業務代碼中通過Feign客戶端調用遠程服務

package cn.tedu.order.service;

import cn.tedu.order.entity.Order;
import cn.tedu.order.feign.AccountClient;
import cn.tedu.order.feign.EasyIdGeneratorClient;
import cn.tedu.order.feign.StorageClient;
import cn.tedu.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;
    @Autowired
    EasyIdGeneratorClient easyIdGeneratorClient;
    @Autowired
    private AccountClient accountClient;
    @Autowired
    private StorageClient storageClient;

    @Override
    public void create(Order order) {
        // 從全局唯一id發號器獲得id
        Long orderId = easyIdGeneratorClient.nextId("order_business");
        order.setId(orderId);

        orderMapper.create(order);

        // 修改庫存
        storageClient.decrease(order.getProductId(), order.getCount());

        // 修改賬戶余額
        accountClient.decrease(order.getUserId(), order.getMoney());
    }
}

啟動項目,訪問測試

訪問訂單項目進行測試:

http://localhost:8083/create?userId=1&productId=1&count=10&money=100

  1. 查看 order、storage和account的控制台查看日志
  2. 查看三個數據庫中的數據變化


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM