SpringCloud事务管理无事务一

Spring Cloud微服务案例分布式事务SeataAT&TCC模式一(无事务控制)

1创建空的java父工程

image-20210701091546723

image-20210701091649902

image-20210701091728185

配置项目的maven

image-20210701092223301

如果项目名称没有显示出来可以选择关闭项目再打开

image-20210701103614549

2创建db-init项目

选中seata-at右键新建module选择springboot项目

image-20210701103905356

image-20210701103952706

image-20210701104006827

db-init的pom文件

<?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.12.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-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>
            <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.properties为yml文件,添加以下配置

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

在resources文件夹下创建sql文件

并添加以下sql文件脚本

image-20210701104342224

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';

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

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;

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';

在启动了类中添加以下内容

	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 {

    public static void main(String[] args) {
        SpringApplication.run(DbInitApplication.class, args);
    }
    
    @Autowired
    private DataSource dataSource;

    /*
    spring 的执行流程:
    包扫描创建所有实例 --- 完成所有的依赖注入 --- @PostConstruct
     */
    @PostConstruct
    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 sql) throws SQLException {
        ClassPathResource cpr = new ClassPathResource(
                sql, DbInitApplication.class.getClassLoader());
        // 处理中文编码
        EncodedResource resource = new EncodedResource(cpr, "UTF-8");

        ScriptUtils.executeSqlScript(
                dataSource.getConnection(),
                resource);
    }
}

image-20210701104928168

执行main方法就会在数据库中创建数据库表

image-20210701105707999

3新建注册中心eureka-server项目

选中seata-at项目右键module,选择springboot项目,

image-20210701111915459

image-20210701112042777

image-20210701112104907

image-20210701112128052

eureka-server的pom文件

<?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.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.tedu</groupId>
    <artifactId>eureka-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>eureka-server</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR11</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>
            <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>

把application.properties文件改为application.yml文件

添加以下配置内容

spring:
  application:
    name: eureka-server

server:
  port: 8761

eureka:
  server:
    #关闭保护模式
    enable-self-preservation: false
  client:
    #对单台服务器,不注册也不拉取
    register-with-eureka: false
    fetch-registry: false

在主启动类添加@EnableEurekaServer注解

@EnableEurekaServer

image-20210701112509787

启动main方法,访问

http://localhost:8761/

可以看到Eureka启动了

image-20210701112625027

4新建order-parent项目

选择seata-at右键新建module,选择springboot项目

image-20210701112759363

image-20210701112825105

image-20210701112837384

image-20210701112852400

image-20210701113148166

order-parent作为父工程

在order-parent中的pom文件添加以下依赖

<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.SR6</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>

image-20210701140443142

order-parent作为父工程可以删掉src文件

5新建account项目模块

选中order-parent项目,右键module,选择springboot项目,

image-20210701140817577

image-20210701140916819

image-20210701140926443

image-20210701140941741

image-20210701141036251

调整此项目依赖的的父工程为order-parent,先把原来依赖的parent删掉,选择alt+insert选择parent,选择order-parent

image-20210701141153653

删掉多余重复的依赖

image-20210701141335276

更改application.propertis配置文件为yml文件,配置内容如下

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

# 8081  account
# 8082  storage
# 8083  order
server:
  port: 8081

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

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

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

添加bootstrap.yml配置网卡(可加可不加)

# 选择网卡
spring:
  cloud:
    inetutils:
      preferred-networks:
        - 192\.168\.245\..+
        #- 10\.1\.6\..+

image-20210701210717675

新建cn.tedu.account.entity包,创建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;  //冻结的金额
}

新建cn.tedu.account.service包,创建AccoutService接口

package cn.tedu.account.service;

import java.math.BigDecimal;

public interface AccoutService {

    void decrease(Long userId, BigDecimal money);
}

创建AccountServiceImpl类,实现AccountService

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 AccoutServiceImpl implements AccoutService{

    @Autowired
    private AccountMapper accountMapper;

    @Override
    public void decrease(Long userId, BigDecimal money) {
        accountMapper.decrease(userId,money);
    }
}

新建cn.tedu.account.mapper包,创建AccountMapper,继承BaseMapper

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

在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>

image-20210701212222324

新建cn.tedu.account.controller包,添加AccountController类

package cn.tedu.account.controller;

import cn.tedu.account.service.AccoutService;
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 AccoutService accoutService;

    @GetMapping("/decrease") //decrease?userId=1&money=100
    public String decrease(Long userId, BigDecimal money){
        accoutService.decrease(userId,money);

        return "ok";
    }
}

在主启动类上加上包扫描注解

@MapperScan("cn.tedu.account.mapper")

image-20210701213321733

运行main方法

运行结果:

可以在注册中心看到服务已经启动

image-20210701185454162

访问

http://localhost:8081/decrease?userId=1&money=100

运行结果:

image-20210701213355020

6新建storage库存项目

选中order-parent项目,新建module,选择springboot项目,

image-20210701151007212

image-20210701151109880

image-20210701151122253

image-20210701151143931

image-20210701151211352

调整pom文件,将原来的parent删掉,alt+inset选择parent,选择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>0.0.1-SNAPSHOT</version>
    </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>
    <properties>
        <java.version>1.8</java.version>
    </properties>

</project>

image-20210701151934391

更改application.properties文件为yml文件,添加配置内容

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

server:
  port: 8082

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}

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

添加bootstrap.yml(可加可不加)

# 选择网卡
spring:
  cloud:
    inetutils:
      preferred-networks:
        - 192\.168\.245\..+
        #- 10\.1\.6\..+

image-20210701184322628

编写业务代码:

新建cn.tedu.storage.entity包

添加Storage实体类

package cn.tedu.storage.entity;

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

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

新建cn.tedu.storage.mapper包,在包中创建StorageMapper接口,继承BaseMapper类,

StorageMapper内容如下:

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

在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>

image-20210701164055294

添加cn.tedu.storage.service包,新建StorageService接口

package cn.tedu.storage.service;

public interface StorageService {

    void decrease(Long productId,Integer count);
}

新建StorageServiceImpl实现类

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) {
        storageMapper.decrease(productId,count);
    }
}

新建cn.tedu.storage.controller包,创建StorageController类

package cn.tedu.storage.controller;

import cn.tedu.storage.service.StorageService;
import com.netflix.discovery.converters.Auto;
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){
        storageService.decrease(productId,count);
        return "减少库存成功";
    }
}

创建文件的结构

image-20210701214831501

在主启动类加上@MapperScan(“cn.tedu.storage.mapper”)注解

@MapperScan("cn.tedu.storage.mapper")

image-20210701214852868

启动main,可以看到已经在eureka上注册了

image-20210701215200984

浏览器访问http://localhost:8082/decrease?userId=1&money=100,可以看到执行成功

image-20210701215309564

7新建order订单模块

选择order-parent项目,右键新建module,选择springboot项目

image-20210701215556543

image-20210701215641008

image-20210701215652376

image-20210701215713841

调整pom依赖,删掉原来的父工程,alt+insert选择parent,选择order-parent,删掉重复的依赖

image-20210701220120990

更改application.properties文件yml文件,添加以下内容

spring:
  application:
    name: order

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

  #事务组的组名
  cloud:
    alibaba:
      seata:
        tx-service-group: order_tx_group

server:
  port: 8083

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}

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

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

添加bootstrap.yml配置网卡

# 选择网卡
spring:
  cloud:
    inetutils:
      preferred-networks:
        - 172\.88\.13\..+
        #- 10\.1\.6\..+

image-20210701220656297

新建cn.tedu.order.entity包,创建Order类

package cn.tedu.order.entity;

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

import java.math.BigDecimal;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
    private Long id;
    private Long userId;
    private Long productId;
    private Integer count;
    private BigDecimal money;
    private Integer status;
}

新建cn.tedu.order.mapper包,创建OrderMapper接口,继承BaseMapper

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

在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>

新建cn.tedu.order.servie包,新建OrderService接口

package cn.tedu.order.servie;

import cn.tedu.order.entity.Order;

public interface OrderService {

    void create(Order order);
}

新建OrderServiceImpl实现类

package cn.tedu.order.servie;

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

    @Override
    public void create(Order order) {
        //TODO: 远程调用发号器 生成订单id 生成订单id
        String s = easyIdClient.nextId("order_business");
        Long orderId = Long.valueOf(s);

        order.setId(orderId);
        orderMapper.create(order);

        //TODO: 调用库存减少商品库存
        storageClient.decrease(order.getProductId(),order.getCount());
        //ToDO: 远程调用账户扣减账户金额
        accountClient.decrease(order.getUserId(),order.getMoney());
    }
}

新建cn.tedu.order.controller包,创建OrderController类

package cn.tedu.order.controller;

import cn.tedu.order.entity.Order;
import cn.tedu.order.servie.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;

    // /create?userId=1&productId=1&count=10&money=100
    @GetMapping("/create")
    public String create(Order order){
        orderService.create(order);
        return "创建订单成功";
    }
}

在主函数上加上@MapperScan(“cn.tedu.order.mapper”)注解

@MapperScan("cn.tedu.order.mapper")

image-20210701223327297

运行main函数,可以看到在eureka上进行了注册

image-20210701223409208

访问

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

可以看到执行成功

image-20210701223458669

8添加全局唯一发号器

分布式系统中,产生唯一流水号的服务系统俗称发号器。

有很多发号器开源项目,这里使用 EasyIdGenerator,具体项目信息请访问:https://github.com/lookingatstarts/easyIdGenerator

下载项目访问 https://github.com/lookingatstarts/easyIdGenerator ,下载发号器项目。

解压到 seata-at 工程目录下

解压,和前面的项目放到同一个工程目录。

image-20210701224549551

把目录改名为 easy-id-generator

image-20210701224647220

导入 Module

在Maven工具窗口中点击添加按钮,选择发号器项目的 pom.xml 文件导入该项目:

注意: 如果右侧没有Maven工具标签,请按两下shift键,然后查找 “add maven projects” 就可以找到这个工具。

image-20210701225025987

image-20210701225121979

pom.xml

发号器向 eureka 进行注册,以便其它服务发现它。

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

		<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

image-20210701225328699

application.yml

  1. 配置使用数据库来生成自增id
  2. 向eureka进行注册
spring:
  application:
    name: easy-id
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka

复制db1.properties更名为seata_order.properties配置内容为

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

将application中的db-list配置的内容改为db-list: [“seata_order”]

image-20210701230354365

同时打开segment关闭snowflake

image-20210701231916785

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:
    enable: 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

数据表

在 db-init 项目的 order.sql 中已经创建了数据表,并插入了一个名为 order_business 的自增id条目。

项目的 schema.sql 中为示例数据表。

删掉easy-id-generator模块中的lombok的版本控制

image-20210701230833664

pom文件

<?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>

        <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>Greenwich.SR6</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

启动项目,可以看到已经在eureka注册了

image-20210701232105789

访问测试

根据 SegmentEasyIdController 类的设置,访问下面地址获取自增id:

http://localhost:9090/segment/ids/next_id?businessType=order_business

image-20210701232323898

9order模块订单添加Feign,调用库存和账户服务

  • 调用发号器获得全局唯一id
  • 调用库存服务减少商品库存
  • 调用账户服务扣减用户金额

application.yml

ribbon 默认超时时间是1秒,为了方便分布式事务测试,把超时时长改为 10 秒:

ribbon:
  ReadTimeout: 10000
spring:
  application:
    name: order

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3307/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
    instance-id: ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port}

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

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

ribbon:
  ReadTimeout: 10000

image-20210701233007690

主程序添加注解启用Feign

添加 @EnableFeignClients 注解:

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

}

image-20210701233359565

在order项目下,

添加Feign声明式客户端接口,发号器的客户端接口:

新建cn.tedu.order.feign包,新建AccountClient接口

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("userId") Long userId,
                    @RequestParam("money") BigDecimal money);
}

新建EasyIdClient接口

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")
public interface EasyIdClient {

    @GetMapping("/segment/ids/next_id")
    String nextId(@RequestParam("businessType")
                          String businessType);
}

新建StorageClient接口

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("productId") Long productId,
                    @RequestParam("count") Integer count);
}

在业务代码中通过Feign客户端调用远程服务

order模块cn.tedu.order.servie包中的OrderServiceImpl业务代码

package cn.tedu.order.servie;


import cn.tedu.order.entity.Order;
import cn.tedu.order.feign.AccountClient;
import cn.tedu.order.feign.EasyIdClient;
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 org.springframework.transaction.annotation.Transactional;

import java.util.Random;

@Service
public class OrderServiceImpl implements OrderService{

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    EasyIdClient easyIdClient;
    @Autowired
    private StorageClient storageClient;
    @Autowired
    private AccountClient accountClient;

    @Override
    public void create(Order order) {
        //TODO: 远程调用发号器 生成订单id 生成订单id
        String s = easyIdClient.nextId("order_business");
        Long orderId = Long.valueOf(s);

        order.setId(orderId);
        orderMapper.create(order);

        //TODO: 调用库存减少商品库存
        storageClient.decrease(order.getProductId(),order.getCount());
        //ToDO: 远程调用账户扣减账户金额
        accountClient.decrease(order.getUserId(),order.getMoney());
    }
}

启动项目,访问测试

访问订单项目进行测试:

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

查看 order、storage和account的控制台查看日志

查看三个数据库中的数据变化

image-20210702202642844

image-20210702202812052

image-20210702202851938

image-20210702202922587

库存比之前减少

image-20210702203122034

订单比之前多一条

image-20210702203229723

image-20210702203329066

l业务代码

package cn.tedu.order.servie;


import cn.tedu.order.entity.Order;
import cn.tedu.order.feign.AccountClient;
import cn.tedu.order.feign.EasyIdClient;
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 org.springframework.transaction.annotation.Transactional;

import java.util.Random;

@Service
public class OrderServiceImpl implements OrderService{

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    EasyIdClient easyIdClient;
    @Autowired
    private StorageClient storageClient;
    @Autowired
    private AccountClient accountClient;

    @Override
    public void create(Order order) {
        //TODO: 远程调用发号器 生成订单id 生成订单id
        String s = easyIdClient.nextId("order_business");
        Long orderId = Long.valueOf(s);

        order.setId(orderId);
        orderMapper.create(order);

        //TODO: 调用库存减少商品库存
        storageClient.decrease(order.getProductId(),order.getCount());
        //ToDO: 远程调用账户扣减账户金额
        accountClient.decrease(order.getUserId(),order.getMoney());
    }
}

启动项目,访问测试

访问订单项目进行测试:

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

查看 order、storage和account的控制台查看日志

查看三个数据库中的数据变化

[外链图片转存中…(img-xSkHapHY-1625230580296)]

[外链图片转存中…(img-Wj2KsO3W-1625230580297)]

[外链图片转存中…(img-bkomGmP2-1625230580297)]

[外链图片转存中…(img-brZyFH3T-1625230580298)]

库存比之前减少

[外链图片转存中…(img-CeZMgvP8-1625230580298)]

订单比之前多一条

[外链图片转存中…(img-0t469jSH-1625230580299)]

[外链图片转存中…(img-RccwYZCn-1625230580300)]

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值