【分布式事务】01--Spring Cloud项目搭建

一,基本介绍

1.什么是分布式事务

首先这是普通事务:
在这里插入图片描述
下面是分布式事务:
在这里插入图片描述
在微服务系统中,每个微服务应用都可能会有自己的数据库,它们首先需要控制自己的本地事务。
一项业务操作可能会调用执行多个微服务。如何保证多个服务执行的多个数据库的操作整体成功或整体失败?这就是分布式事务要解决的问题。

2.理论部分

CAP 和 BASE 是对大规模互联网系统分布式实践的理论总结。

CAP

CAP原则
在这里插入图片描述
在分布式系统中,由于网络原因出现子系统之间无法通信的情况,就会造成分区。一般分布式系统中必须容忍这种情况,那么就需要在A和C之间进行取舍。
在分布式事务中

  • 如果保证CP,就意味着要让所有子系统的数据操作要么全部成功,要么全部失败,不允许有不一致的情况发生。但是强一致性会造成性能下降。
  • 如果保证AP,就意味着可以牺牲一定的一致性,允许在各个子系统中存在有的数据操作成功,有的数据操作失败的情况,只要通过后续处理,能够达到最终一致即可。

BASE

BASE

3.分布式事务方案

分布式事务有以下解决方案:

  • XA
  • TCC
  • Seata 框架 AT 事务
  • SAGA
  • 可靠消息最终一致性
  • 最大努力通知
    在这里插入图片描述

二,业务场景

订单系统
在这里插入图片描述
当用户下订单时,执行以下三步流程:

  • 订单系统保存订单
  • 订单系统调用库存服务,减少商品库存
  • 订单系统调用账户服务,扣减用户金额
    这三步要作为一个整体事务进行管理,要么整体成功,要么整体失败。

三,创建父项目

在这里插入图片描述
在这里插入图片描述

三,数据库初始化工具

1.订单案例涉及四个数据库:

在这里插入图片描述
为了后续测试方便我们编写一个工具,用来重置所有数据库表,可以方便地把数据重置到初始状态。

2.创建springboot子项目module

在这里插入图片描述
添加数据库相关依赖
在这里插入图片描述

3.修改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>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.配置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.添加 sql 脚本文件

下面,在 resources 目录下,先新建一个 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';

6.主程序中添加代码,执行sql脚本

Spring 中提供了一个 jdbc 脚本执行器,使用这个工具可以非常方便的运行一个 sql 脚本文件,下面是这个方法:
ScriptUtils.executeSqlScript()
只需要传入它需要的参数即可。
下面代码运行 sql 目录中的四个脚本程序,每次运行都会删除四个数据库再重新创建,并初始化数据。

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 提供的一个工具,来执行sql脚本文件
        ScriptUtils.executeSqlScript(dataSource.getConnection(), resource);
    }
}

启动项目,数据库刷新成功
在这里插入图片描述

四,eureka注册中心

1.新建springboot Module:eureka-server

在这里插入图片描述
添加依赖
在这里插入图片描述

2.修改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.配置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.启动类添加@EnableEurekaServer

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.测试

启动项目
在这里插入图片描述

五,创建微服务的父项目

1.创建Maven的 module

在这里插入图片描述

2.修改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>

六,account账户项目

1.在order-parent下创建子项目accout,springboot,Module

在这里插入图片描述
注意路径
在这里插入图片描述

2.修改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>account</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>account</name>

</project>

3.配置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  #驼峰命名    
logging:                #打印日志
  level: 
    com.drhj.account.mapper: debug

4.配置bootstrap.yml,指定网段

spring:
  cloud:
    inetutils:
      preferred-networks:
        - 10\.1\.6\..+
        - 192\.168\.0\..+

5.添加实体类

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;          //用户id
    private BigDecimal total;     //总数
    private BigDecimal used;      //已使用,已消费金额
    private BigDecimal residue;   //可用金额
    private BigDecimal frozen;    //冻结金额
}

6.创建AccountMapper接口

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> {
    //扣除账户金额
    void decrease(Long userId, BigDecimal money);
}

7.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="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.主程序添加 Mybatis 扫描注解

添加注解 @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.添加 AccountService 接口和它的实现类

decrease() 方法实现扣减账户金额的功能

package com.drhj.account.service;

import java.math.BigDecimal;

public interface AccountService {
    //扣减账户
    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.添加 AccountController 类提供客户端访问接口

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 "扣减账户成功";
    }
}

11.测试

1)查看 eureka 注册信息
访问 http://localhost:8761/ 查看账户服务在 eureka 中的注册信息:
在这里插入图片描述
2)访问账户服务执行账户扣减金额
访问 http://localhost:8081/decrease?userId=1&money=100
在这里插入图片描述
3)查看控制台 Mybatis 执行的 sql 日志
在这里插入图片描述
4)查看数据库表,确认金额已经被减掉
在这里插入图片描述

七,storage库存项目

storage 库存微服务项目,用来实现减少库存的功能。

1.新建 springboot的Module:storage

在这里插入图片描述
在这里插入图片描述

2.修改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.配置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  #驼峰命名
logging:                #打印日志
  level:
    com.drhj.storage.mapper: debug   

4.配置bootstrap.yml

spring:
  cloud:
    inetutils:
      preferred-networks:
        - 10\.1\.6\..+
        - 192\.168\.0\..+

5.创建实体类

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;   //商品id
    private Integer total;    //总数
    private Integer used;     //已使用,已售出
    private Integer residue;  //可用库存
    private Integer frozen;   //冻结库存
}

6.创建 StorageMapper 接口

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> {
    //减少库存
    void decrease(Long productId,Integer count);
}

7.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="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.主程序添加 Mybatis 扫描注解

添加注解 @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.添加 StorageService 接口和它的实现类

decrease() 方法实现减少商品库存功能。

package com.drhj.storage.service;

public interface StorageService {
    //减少库存
    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.添加 StorageController 类提供客户端访问接口

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 "减少库存成功";
    }
}

11.测试

1)查看 eureka 注册信息
访问 http://localhost:8761/ 查看库存服务在 eureka 中的注册信息:
在这里插入图片描述
2)访问库存服务,执行减少库存操作
访问 http://localhost:8082/decrease?productId=1&count=1
在这里插入图片描述
3)查看数据库表,确认金额已经被减掉
在这里插入图片描述

八,order订单项目

order 订单项目保存订单,并调用 storage 和 account 减少库存和扣减金额。

1.新建 springboot 的 Module:order

在这里插入图片描述
在这里插入图片描述

2.修改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.配置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  #驼峰命名
logging:                #打印日志
  level:
    com.drhj.order.mapper: debug
ribbon:
  MaxAutoRetriesNextServer: 0 #关闭重试 禁用Ribbon重试,防止对分布式事务有影响   

4.配置bootstrap.yml

spring:
  cloud:
    inetutils:
      preferred-networks:
        - 10\.1\.6\..+
        - 192\.168\.0\..+

5.创建order实体类

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;      //用户id
    private Long productId;   //商品id
    private Integer count;    //购买的数量
    private BigDecimal money; //花多少钱
    private Integer status;   //状态,0-冻结,1-正常
}

6.创建OrderMapper接口

package com.drhj.order.mapper;

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

public interface OrderMapper extends BaseMapper<Order> {
    //创建订单
    void create(Order order);
}

7.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="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.主程序添加 Mybatis 扫描注解

添加注解 @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.添加 OrderService 接口和它的实现类

create() 方法实现保存订单的功能。

package com.drhj.order.service;

import com.drhj.order.entity.Order;

public interface OrderService {
    //创建订单
    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) {
        //远程调用发号器,生成订单id
        //先临时随机产生id,加了发号器后,这行代码删除
        Long id = Math.abs(new Random().nextLong());
        order.setId(id);
        orderMapper.create(order);
        //远程调用库存,减少库存
        //远程调用账户,扣减账户
    }
}

10.添加 OrderController 类提供客户端访问接口

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 "创建订单成功";
    }
}

11.测试

1)查看 eureka 注册信息
访问 http://localhost:8761/ 查看订单服务在 eureka 中的注册信:
在这里插入图片描述
2)访问订单服务,执行订单保存
访问 http://localhost:8083/create?userId=1&productId=1&count=10&money=100
在这里插入图片描述
3)查看控制台 Mybatis 执行的 sql 日志
在这里插入图片描述
4)查看数据库表,确认订单保存成功
在这里插入图片描述

九,全局唯一id发号器

分布式系统中,产生唯一流水号的服务系统俗称发号器。
有很多发号器开源项目,这里使用 EasyIdGenerator,具体项目信息请访问
https://github.com/lookingatstarts/easyIdGenerator

1.下载项目

访问https://github.com/lookingatstarts/easyIdGenerator,下载发号器项目。
在这里插入图片描述
解压放到我们父项目目录下,并修改名称
在这里插入图片描述

2.导入module

1)直接将pom.xml文件拖入idea中
在这里插入图片描述
2)修改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)右键 --> add as maven project
在这里插入图片描述

3.配置application.yml

因为我们自己创建了数据库,所以不使用他的数据库

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:          #使用数据库产生id
    enable: true
    db-list: "seata_order"                      #["db1","db2"]
    fetch-segment-retry-times: 3 # 从数据库获取号段失败重试次数
spring:
  application:
    name: easy-id
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    prefer-ip-address: true

4.配置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.配置bootstrap.yml

spring:
  cloud:
    inetutils:
      preferred-networks:
        - 10\.1\.6\..+
        - 192\.168\.0\..+

7.查看其使用数据库控制层方法

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年05月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.测试

1)启动服务
2)查看 eureka 中的注册信息
在这里插入图片描述
3)根据 SegmentEasyIdController 类的设置,访问下面地址获取自增id:
http://localhost:9090/segment/ids/next_id?businessType=order_business
谷歌一般看不到,可以右键 --> 查看
或者使用其他浏览器
在这里插入图片描述

十,远程调用配置

1.业务分析

当一个订单模块(order)生成时,首先系统得给他一个全局唯一的id,相应的,个人账户对应的资金也会减少,商品库存也会减少;
所以需要远程调用全局唯一id生成模块(easy-id-generator),账户模块(accout),库存模块(storage);

2.添加依赖

由于父项目中order-parent已经下载好对应的依赖,所以这里不用再进行添加。

3.启动项添加注解

因为是order项目调用其他项目,所以在order的启动项上添加@EnableFeignClients

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.添加Feign声明式客户端接口

发号器的客户端接口:

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") //对应eureka注册表中名称,大小写不敏感
public interface EasyIdClient {
    @GetMapping("/segment/ids/next_id")
    String nextId(@RequestParam("businessType") String businessType);
}

账户服务的客户端接口:

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);
}

库存服务的客户端接口:

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.在order业务代码中通过Feign客户端调用远程服务

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) {
        //远程调用发号器,生成订单id
        String s = easyIdClient.nextId("order_business");
        Long id = Long.valueOf(s);
        order.setId(id);
        orderMapper.create(order);
        //远程调用库存,减少库存
        storageClient.decrease(order.getProductId(),order.getCount());
        //远程调用账户,扣减账户
        accountClient.decrease(order.getUserId(),order.getMoney());
    }
}

6.测试

启动所有项目,访问订单项目进行测试:
http://localhost:8083/create?userId=1&productId=1&count=10&money=100
1)查看执行结果
在这里插入图片描述
2)查看控制台日志
order
在这里插入图片描述
account
在这里插入图片描述
storage
在这里插入图片描述
3)查看三个数据库中的数据变化
order
在这里插入图片描述
account
在这里插入图片描述
storage
在这里插入图片描述

十一,备份

在这里插入图片描述
至此,项目搭建完毕。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值