SpringCloud+Nacos1.4.2+Seata1.3.0实现分布式事务以及踩坑总结

本文详细介绍了Seata分布式事务解决方案,包括Seata的基本概念、安装部署步骤、业务数据库及微服务准备、测试流程以及Seata的工作原理等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

一、分布式事务问题

分布式之前:
系统单机单库,不会出现分布式问题,随着业务的发展,系统越来越复杂,数据库也开始变化由一对一逐渐演变一对多,多对多,此时一个单一的系统无法支撑起整个应用,此时,系统逐渐被拆分成一个一个服务,每一个应用组成一个服务,微服务诞生。
分布式之后:
单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源。业务操作需要调用三个服务来完成,此时每个服务内部的数据一致性由本地本地事务来保证,但是全局的数据一致性问题没法保证

示意图如下
在这里插入图片描述
总之一句话:一次业务操作需要垮多个数据源或垮多个系统进行远程调用,就会产生分布式事务问题

二、Seata简介

1,是什么

官网地址https://seata.io/zh-cn/
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。

2,能干嘛

1,术语
一个典型的分布式事务过程:分布式事务处理过程的 一ID+三组件模型(简称1+3模型)

  1. XID (Transaction ID) : 全局唯一的事务ID

  2. TC (Transaction Coordinator) - 事务协调者
    维护全局和分支事务的状态,驱动全局事务提交或回滚。

  3. TM (Transaction Manager) - 事务管理器
    定义全局事务的范围:开始全局事务、提交或回滚全局事务。

  4. RM (Resource Manager) - 资源管理器
    管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

2,处理过程

  1. TM 向TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID

  2. XID 在微服务调用链路的上下文中传播

  3. RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖

  4. TM 向TC 发起针对 XID 的全局提交或回滚决议

  5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求

    在这里插入图片描述

3,去哪下

下载地址https://seata.io/zh-cn/blog/download.html
在这里插入图片描述

4,怎么玩

单个应用我们只需要使用一个注解 @Transactional
分布式我们也只需要一个注解 @GlobalTransactional
用起来就这么简单,搞定!!!!

ps 往往使用起来越简单,往往越不简单

在这里插入图片描述

三、Seata-Server安装

1,官网地址

地址 https://seata.io/zh-cn/index.html

2,下载版本

下载地址 https://seata.io/zh-cn/blog/download.html

3,解压修改file.conf文件

先备份初始文件再修改
我们这里使用数据库(版本不一致,file.conf也可能不一样)
store修改如下
在这里插入图片描述

4,数据库新建seata库

新版本的seata1.0以后没有sql文件需要自行去查找
链接地址 https://github.com/seata/seata/tree/develop/script

5,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;

6,修改seata-server的registry.conf配置文件

在这里插入图片描述
后面我们会将配置上传到nacos,所以配置中心也需要修改如下
在这里插入图片描述

7,启动nacos端口8848

8,启动seata-server

在这里插入图片描述

启动时可能报错
(1)会报日志文件找不到的错误
解决办法创建一个logs文件,在文件目录下新建一个seata_gc.log
(2)报内存不够(报文未截图,后续补充下)
解决办法 修改启动参数
在这里插入图片描述
将其设置为1024,1024,1024,512即可

9,将配置导入到nacos

(1)修改 config.tx将配置注册到nacos中
下载下来是没有这个文件的所以要自己去下载https://github.com/seata/seata/tree/1.3.0/script/config-center

启动脚本以及config.txt都有,注意目录结构不要放错了,否则启动不起来,或者自行修改启动脚本
在这里插入图片描述

(2)执行将config.txt推送到nacos中

sh nacos-config.sh -h 182.92.219.202 -p 8848 -g SEATA_GROUP -u nacos -w nacos -t 51915a62-d2d6-43d4-8f45-86b159eb90f5

参数配置说明如下


sh ${SEATAPATH}/script/config-center/nacos/nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t 5a3c7d6c-f497-4d68-a71a-2e5e3340b3ca -u username -w password
-h: host, the default value is localhost.

-p: port, the default value is 8848.

-g: Configure grouping, the default value is 'SEATA_GROUP'.

-t: Tenant information, corresponding to the namespace ID field of Nacos, the default value is ''.

-u: username, nacos 1.2.0+ on permission control, the default value is ''.

-w: password, nacos 1.2.0+ on permission control, the default value is ''.

在这里插入图片描述

四、业务数据库准备

订单-库存-账户 业务数据库准备

1,前提条件

需要先启动Nacos后再启动Seata,两个服务必须先启动起来

2,分布式事务业务说明

参考地址 https://github.com/seata/seata-samples/tree/master/seata-spring-boot-starter-samples
流程如下:
下订单–>扣库存–>减账户余额

创建三个服务:订单服务、库存服务、账户服务
具体流程如下:
当用户下叮当时,会在订单服务中创建一个订单,然后通过远程调用库存服务开扣减下单商品的库存,再通过远程调用账户服务来扣减用户账户里面的余额,最后再订单服务中修改订单状态为已完成
流程说明:跨越三个数据库,有两次远程调用,会存在分布式事务问题

3,创建业务数据库

创建三个数据库

CREATE DATABASE 数据库名

存储订单的数据库 seata_order
存储库存的数据库 seata_storage
存储账户信息的数据库 seata_account

4,按照业务数据库创建业务表

存储订单的数据库 seata_order中

CREATE TABLE `t_order` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint DEFAULT NULL COMMENT '用户id',
  `product_id` bigint DEFAULT NULL COMMENT '产品id',
  `count` int DEFAULT NULL COMMENT '数量',
  `money` decimal(11,0) DEFAULT NULL COMMENT '金额',
  `status` int DEFAULT NULL COMMENT '订单状态:0:创建中;1:已完结',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表';


存储库存的数据库 seata_storage中

CREATE TABLE `t_storage` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `product_id` bigint DEFAULT NULL COMMENT '产品id',
  `total` int DEFAULT NULL COMMENT '库存',
  `used` int DEFAULT NULL COMMENT '已用库存',
  `residue` int DEFAULT NULL COMMENT '剩余库存',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='库存表';


存储账户信息的数据库 seata_account中

CREATE TABLE `t_account` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint DEFAULT NULL COMMENT '用户id',
  `total` decimal(10,0) DEFAULT NULL COMMENT '总额度',
  `used` decimal(10,0) DEFAULT NULL COMMENT '已用额度',
  `residue` decimal(10,0) DEFAULT NULL COMMENT '剩余可用额度',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户表';


5,按照业务数据库创建对应的回滚日志表

每个库建一个
订单、库存、账户下都需要建各自的回滚日志表

CREATE TABLE `undo_log` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `branch_id` bigint NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=utf8;


6,最终效果

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

在这里插入图片描述

五、业务微服务准备

订单-库存-账户 业务微服务准备

1,业务需求

下订单–>减库存–>扣余额–>改(订单)状态

2,新建订单Order-Module

源码地址 https://gitee.com/jn-acheng/spring-cloud-study/tree/master/spring-cloud-seata-order-8101
(1)新建module
spring-cloud-seata-order-8101
版本 (父pom)
在这里插入图片描述

(2)POM
只列出核心pom依赖

<dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

(3)YML

server:
  port: 8101
spring:
  application:
    name: seata-order
  datasource:
    url: jdbc:mysql://192.168.119.50:3306/seata_order?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.119.50:8848
        username: nacos
        password: nacos
        group: SEATA_GROUP # 分组
#        namespace: 193f80d6-57e9-4718-87ff-179335a50ac5 # 命名空间
#  sh nacos-config.sh -h 192.168.119.50 -p 8848 -g SEATA_GROUP -u nacos -w nacos -t 292c3d9d-b037-49e5-bfc6-1f14d648c743
seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_test_tx_group  #这里要特别注意和nacos中配置的要保持一直
  registry:
    type: nacos
    nacos:
      serverAddr: ${spring.cloud.nacos.discovery.server-addr}
      username: ${spring.cloud.nacos.discovery.username}
      password: ${spring.cloud.nacos.discovery.password}
      group: ${spring.cloud.nacos.discovery.group}
  config:
    type: nacos
    nacos:
      server-addr: ${spring.cloud.nacos.discovery.server-addr}
      username: ${spring.cloud.nacos.discovery.username}
      password: ${spring.cloud.nacos.discovery.password}
      group: ${spring.cloud.nacos.discovery.group}
  service:
    vgroup-mapping:
      my_test_tx_group: default
management:
  endpoints:
    web:
      exposure:
        include: '*'
mybatis:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

(4)代码
1,主启动类

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class SpringBootSeataOrder_8101 {
     public static void main(String[] args) {
           SpringApplication.run(SpringBootSeataOrder_8101.class, args);
      }

}

2,订单pojo类

@Data
public class Order {
  private Long  id;
    /**
     * 用户id
     */
  private Long  userId ;
    /**
     * 产品id
     */
  private Long  productId;
    /**
     * 数量
     */
  private int  count;
    /**
     * '金额'
     */
  private BigDecimal money;
    /**
     * 订单状态 0:创建中;1:已完结
     */
  private int  status;
}

3,OrderMapper

@Mapper
public interface OrderMapper {
    /**
     * 创建订单
     * @param order
     */
    void create(Order order);

    /**
     * 修改订单状态
     * @param id
     * @param status
     */
    void update(@Param("id") Long id,@Param("status") int status);
}

4,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.acheng.mapper.OrderMapper">
    <resultMap id="BaseResultMap" type="com.acheng.pojo.Order">
        <id column="id" property="id" jdbcType="BIGINT"></id>
        <result column="user_id" property="userId" jdbcType="BIGINT" />
        <result column="product_id" property="productId" jdbcType="BIGINT" />
        <result column="count" property="count" jdbcType="BIGINT" />
        <result column="money" property="money" jdbcType="DECIMAL" />
        <result column="status" property="status" jdbcType="INTEGER" />
    </resultMap>
    <insert id="create" useGeneratedKeys="true" keyProperty="id">
        insert into t_order (user_id,product_id,count,money,status) values
        (#{userId},#{productId},#{count},#{money},#{status})
    </insert>
    <update id="update">
        update  t_order set status=#{status} where id=#{id}
    </update>
</mapper>

5,service接口,有三个接口
OrderService

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

StorageService

@FeignClient("seata-storage")
public interface StorageService {
    /**
     * 扣减库存
     */
    @PostMapping("/storage/decrease")
    public void decrease(@RequestParam("productId") Long productId, @RequestParam("count")int count);
}

AccountService

@FeignClient(value ="seata-account" )
public interface AccountService {
    /**
     * 扣减金钱
     */
    @PostMapping("/account/decrease")
    public void decrease(@RequestParam("userId") Long userId,@RequestParam("used") BigDecimal used);
}

6,实现类

package com.acheng.service.impl;

import com.acheng.mapper.OrderMapper;
import com.acheng.pojo.Order;
import com.acheng.service.AccountService;
import com.acheng.service.OrderService;
import com.acheng.service.StorageService;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author LiuCheng
 * @data 2021/5/13 15:53
 */
@Service
@Slf4j
@GlobalTransactional(name = "my_test_tx_group" ,rollbackFor = Exception.class)
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private StorageService storageService;
    @Autowired
    private AccountService accountService;
    @Override
    public void create(Order order) {
        // 创建订单
        log.info("======= 创建订单  start");
        orderMapper.create(order);
        // 扣减库存
        log.info("======= 扣减库存  start");
        storageService.decrease(order.getProductId(),order.getCount());
        log.info("======= 扣减库存   end");
        // 扣减账户余额
        log.info("======= 扣减账户余额  start");
        accountService.decrease(order.getUserId(),order.getMoney());
        log.info("======= 扣减账户余额   end");
        // 修改订单状态
        log.info("======= 修改订单状态   start");
        int status=1;
        orderMapper.update(order.getId(),1);
        log.info("======= 修改订单状态   end");
        log.info("======= 创建订单   end");

    }
}

7,控制层

@RestController
@RequestMapping("/order")
public class OrderController {
    @Autowired
    OrderService orderService;
    @GetMapping("/create")
    public CommonResult create(Order order){
        orderService.create(order);
        return new CommonResult(200,"订单创建成功");
    }
}

3,新建库存Storage-Module

偷个懒,不贴代码了 o(╯□╰)o
源码地址 https://gitee.com/jn-acheng/spring-cloud-study/tree/master/spring-cloud-seata-storage-8201

4,新建账户Account-Module

再偷个懒,不贴代码了 o(╯□╰)o
源码地址 https://gitee.com/jn-acheng/spring-cloud-study/tree/master/spring-cloud-seata-account-8301

六、测试

下订单–>减库存–>扣余额–>改(订单)状态

1,数据库初始情况

在这里插入图片描述

2,正常下单

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

3,超时异常没加@GloblaTransactional

在这里插入图片描述
在这里插入图片描述
订单状态时0 未支付
在这里插入图片描述
库存被扣减了
在这里插入图片描述
账户余额扣减了
在这里插入图片描述
当库存和账户金额扣减后,订单状态并没有设置为已完成,没有从0改为1,而且feign的重试机制,账户余额还可能多次扣减

4,超时异常添加加@GloblaTransactional

在这里插入图片描述

在这里插入图片描述
订单未插入
在这里插入图片描述
库存未减少
在这里插入图片描述

金额未扣减
在这里插入图片描述

七、补充Seata原理

1,Seata

Seata是Fescar的升级版本,2019年1月份蚂蚁金服和阿里巴巴共同开元的分布式解决方案 Seata
全称 Simple Extensible Autonomous Transaction Architecture。

2,再看TC/TM/RM三大组件

在这里插入图片描述
在这里插入图片描述
分布式事务的执行流程

  • TM 开启分布式事务(TM向TC注册全局事务记录)
  • 按业务场景,编排数据库、服务等事务内资源(RM向TC汇报资源准备状态)
  • TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚分布式事务)
  • TC汇总事务信息,决定分布式事务是提交还是回滚
  • TC通知所有RM 提交/回滚 资源,事务二阶段结束

3,AT模式如何做到对业务的无侵入

(1)是什么

  • 基于支持本地 ACID 事务的关系型数据库
  • Java 应用,通过 JDBC 访问数据库。

整体机制
两阶段提交协议的演变:

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。

  • 二阶段:

1,提交异步化,非常快速地完成。
2,回滚通过一阶段的回滚日志进行反向补偿,

(2)一阶段加载

在一阶段,Seata会拦截 “业务SQL”

  1. 解析SQL语义,找到 "业务SQL"要更新的业务数据,在业务数据被更新前,将其保存成 “before image” 前置镜像
  2. 执行 “业务SQL” 更新业务数据,在业务数据更新之后
  3. 将保存成 “after image” 后置镜像,最后生成行锁

以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性
在这里插入图片描述

1,解析 SQL:得到 SQL 的类型(UPDATE),表(product),条件(where name = ‘TXC’)等相关的信息。
2,查询前镜像:根据解析得到的条件信息,生成查询语句,定位数据。

select id, name, since from product where name = 'TXC';

在这里插入图片描述
3,执行业务 SQL:更新这条记录的 name 为 ‘GTS’。
4,查询后镜像:根据前镜像的结果,通过 主键 定位数据。

select id, name, since from product where id = 1;

在这里插入图片描述
5,插入回滚日志:把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录,插入到 UNDO_LOG 表中。

{
	"branchId": 641789253,
	"undoItems": [{
		"afterImage": {
			"rows": [{
				"fields": [{
					"name": "id",
					"type": 4,
					"value": 1
				}, {
					"name": "name",
					"type": 12,
					"value": "GTS"
				}, {
					"name": "since",
					"type": 12,
					"value": "2014"
				}]
			}],
			"tableName": "product"
		},
		"beforeImage": {
			"rows": [{
				"fields": [{
					"name": "id",
					"type": 4,
					"value": 1
				}, {
					"name": "name",
					"type": 12,
					"value": "TXC"
				}, {
					"name": "since",
					"type": 12,
					"value": "2014"
				}]
			}],
			"tableName": "product"
		},
		"sqlType": "UPDATE"
	}],
	"xid": "xid:xxx"
}

6,提交前,向 TC 注册分支:申请 product 表中,主键值等于 1 的记录的 全局锁 。
7,本地事务提交:业务数据的更新和前面步骤中生成的 UNDO LOG 一并提交。
8,将本地事务提交的结果上报给 TC。

(3)二阶段提交

在这里插入图片描述

1,收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
2,异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。

(4)二阶段回滚

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

1,收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
2,通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
3,数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。
4,根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:

update product set name = 'TXC' where id = 1;

5,提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。

4,debug调试查看

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

在这里插入图片描述
在这里插入图片描述
debug放行后,数据删除
在这里插入图片描述

5,补充总结

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

总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值