微服务架构 --- 使用Seata处理分布式事务

目录

引言

一.Seata 是什么?

1.Seata的核心架构: 

2. Seata的分布式事务处理流程:

 二.Seata的基本使用:

1.环境准备:

(1)单机部署: 

 (2)集群部署:

【1】初始化数据库

【2】设置使用 Nacos 注册中心

【3】启动 TC Server

(3)Windows部署:

2.引入依赖:

3.加入配置来使用Seata:

(1)首先在nacos上添加一个共享的seata配置,命名为shared-seata.yaml:

(2)改造模块,添加bootstrap.yaml:

(3)改造application.yaml文件:

三.XA模式:

1.处理流程:

2.注解使用: 

使用 @GlobalTransactional 注解全局事务:

使用 @Transactional 来支持分布式事务的回滚:


引言

在正常的项目中,如果出现了多个微服务操作数据库需要回滚的情况下,这种情况就是分布式事务。我们可以引入下面依赖,随后使用 @Transactional 加入事务处理:

首先引入jdbc依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

然后再该类上加入@Transactional来使用Spring布置事务:

import cn.eleven.product.bean.Product;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;

@Service
public class ProductService {
    @Transactional
    public void deduct(int count) {
        // 业务扣减逻辑
        // ...

        // 假设执行完成业务扣减逻辑后抛出异常
        if(count > 0) {
            throw new RuntimeException("库存不足");
        }
    }
}

 最后在启动类加入注解@EnableTransactionManagement来开启事务管理:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@EnableTransactionManagement // 开启事务管理
@SpringBootApplication
public class ProductMainApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProductMainApplication.class,args);
    }
}

所以我们可以将所有的调用数据库的代码给其加入注解来处理事务。

但是这样是无法解决分布式事务问题,原因在于如果我们如下代码再业务逻辑插入远程调用:

@Service
public class ProductService {
	@Autowired
	private UserMapper userMapper;
	@Autowired
	private AccountFeignClient accountFeignClient;
    @Transactional
    public void deduct(int count) {
        // 业务扣减逻辑
        // 远程调用(远程调用加入@Transactional没有用)
        AccountFeignClient.debut(count);
        // 本地调用数据库
        userMapper.insert(count);

        // 假设执行完成业务扣减逻辑后抛出异常
        int i = 10 / 0;
    }
}

但是由于远程调用加入@Transactional没有用,而对于【userMapper.set(count);】这段代码,由于deduct()方法添加@Transactional,那么这个就会连上自己的订单库去插入订单,但是一旦发生异常,就相当于连上这个订单库就炸了,所以就只会回滚订单的插入,而远程调用没有添加注解也就不会回滚(加了也没用,远程调用加入@Transactional没有用)

@Transactional 是 ​本地事务​ 管理注解,它只能管理 ​单个数据库连接​ 的事务,而无法跨服务协调多个资源的事务。

// 只能控制这个本地数据库操作
userMapper.insert(count); 

// 无法控制这个远程调用的事务性
accountFeignClient.debut(count); 

在我们的示例代码中:

@Transactional
public void deduct(int count) {
    accountFeignClient.debut(count); // 远程调用 → 不受事务控制
    userMapper.insert(count);        // 本地数据库 → 受事务控制
    int i = 10 / 0;                  // 抛出异常
}

会发生以下情况:

  1. 远程调用 accountFeignClient.debut(count) ​立即生效,无法回滚
  2. 本地 userMapper.insert(count) 会因异常被回滚
  3. 最终结果:账户扣减已生效,但本地记录未保存 → ​数据不一致

【总结:会出现部分回滚情况而造成数据不一致现象】


一.Seata 是什么?

Seata 是一个开源的分布式事务解决方案,旨在帮助开发者管理分布式系统中的全局事务。它特别适用于微服务架构,在分布式环境下,保证多个服务参与的事务的最终一致性

其实分布式事务产生的一个重要原因,就是参与事务的多个分支事务互相无感知,不知道彼此的执行状态。因此解决分布式事务的思想非常简单:

就是找一个统一的事务协调者(TC),与多个分支事务通信,检测每个分支事务的执行状态,保证全局事务下的每一个分支事务同时成功或失败即可。大多数的分布式事务框架都是基于这个理论来实现的。

下面是Seata的官网文档:Seata官网

1.Seata的核心架构: 

Seata 的核心架构由以下三个组件组成:

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

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

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

其中,TM RM 可以理解为 Seata 的客户端部分,引入到参与事务的微服务依赖中即可。将来 TM RM 就会协助微服务,实现本地分支事务与 TC 之间交互,实现事务的提交或回滚。而 TC 服务则是事务协调中心,是一个独立的微服务,需要单独部署。

2. Seata的分布式事务处理流程:

Seata 的分布式事务处理流程大致如下:

  1. TM 开启全局事务:微服务的一个模块(通常是一个主调用方)发起全局事务,TM 会通知 TC 开启一个全局事务。
  2. RM 注册分支事务:在全局事务的过程中,各个参与的微服务会向 TC 注册自己的分支事务。
  3. 事务提交或回滚:TM 提交或回滚全局事务时,TC 会通知各个 RM 提交或回滚分支事务,最终完成整个全局事务的处理。

其中,TMRM可以理解为Seata的客户端部分,引入到参与事务的微服务依赖中即可。将来TMRM就会协助微服务,实现本地分支事务与TC之间交互,实现事务的提交或回滚。而TC服务则是事务协调中心,是一个独立的微服务,需要单独部署。

 二.Seata的基本使用:

1.环境准备:

(1)单机部署: 

 打开 Seata 下载页面,选择想要的 Seata 版本。这里,我们选择 v1.1.0 最新版本。

# 创建目录
$ mkdir -p /Users/yunai/Seata
$ cd /Users/yunai/Seata

# 下载
$ wget https://github.com/seata/seata/releases/download/v1.1.0/seata-server-1.1.0.tar.gz

# 解压
$ tar -zxvf seata-server-1.1.0.tar.gz

# 查看目录
$ cd seata
$ ls -ls
24 -rw-r--r--    1 yunai  staff  11365 May 13  2019 LICENSE
 0 drwxr-xr-x    4 yunai  staff    128 Apr  2 07:46 bin # 执行脚本
 0 drwxr-xr-x    9 yunai  staff    288 Feb 19 23:49 conf # 配置文件
 0 drwxr-xr-x  138 yunai  staff   4416 Apr  2 07:46 lib #  seata-*.jar + 依赖库

随后启动TC Server:

执行 nohup sh bin/seata-server.sh & 命令,启动 TC Server 在后台。在 nohup.out 文件中,我们看到如下日志,说明启动成功:

# 使用 File 存储器
2020-04-02 08:36:01.302 INFO [main]io.seata.common.loader.EnhancedServiceLoader.loadFile:247 -load TransactionStoreManager[FILE] extension by class[io.seata.server.store.file.FileTransactionStoreManager]
2020-04-02 08:36:01.302 INFO [main]io.seata.common.loader.EnhancedServiceLoader.loadFile:247 -load SessionManager[FILE] extension by class[io.seata.server.session.file.FileBasedSessionManager]
# 启动成功
2020-04-02 08:36:01.597 INFO [main]io.seata.core.rpc.netty.RpcServerBootstrap.start:155 -Server started ...

默认配置下,Seata TC Server 启动在 8091 端点。

因为我们使用 file 模式,所以可以看到用于持久化的本地文件 root.data。操作命令如下:

$ ls -ls sessionStore/
total 0
0 -rw-r--r--  1 yunai  staff  0 Apr  2 08:36 root.data

 (2)集群部署:

本小节,我们来学习部署集群 Seata TC Server,实现高可用,生产环境下必备。在集群时,多个 Seata TC Server 通过 db 数据库,实现全局事务会话信息的共享。

同时,每个 Seata TC Server 可以注册自己到注册中心上,方便应用从注册中心获得到他们。最终我们部署 集群 TC Server 如下图所示:

集群 TC Server

Seata TC Server 对主流的注册中心都提供了集成,具体可见 discovery 目录。考虑到国内使用 Nacos 作为注册中心越来越流行,这里我们就采用它。

打开 Seata 下载页面,选择想要的 Seata 版本。这里,我们选择 v1.1.0 最新版本。

# 创建目录
$ mkdir -p /Users/yunai/Seata
$ cd /Users/yunai/Seata

# 下载
$ wget https://github.com/seata/seata/releases/download/v1.1.0/seata-server-1.1.0.tar.gz

# 解压
$ tar -zxvf seata-server-1.1.0.tar.gz

# 查看目录
$ cd seata
$ ls -ls
24 -rw-r--r--    1 yunai  staff  11365 May 13  2019 LICENSE
 0 drwxr-xr-x    4 yunai  staff    128 Apr  2 07:46 bin # 执行脚本
 0 drwxr-xr-x    9 yunai  staff    288 Feb 19 23:49 conf # 配置文件
 0 drwxr-xr-x  138 yunai  staff   4416 Apr  2 07:46 lib #  seata-*.jar + 依赖库
【1】初始化数据库

① 使用 mysql.sql 脚本,初始化 Seata TC Server 的 db 数据库。脚本内容如下: 

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

在 MySQL 中,创建 seata 数据库,并在该库下执行该脚本。最终结果如下图:

 数据库 - MySQL 5.X

② 修改 conf/file 配置文件,修改使用 db 数据库,实现 Seata TC Server 的全局事务会话信息的共享。如下图所示:

 配置文件

③ MySQL8 的支持

 首先,需要下载 MySQL 8.X JDBC 驱动,命令行操作如下:

$ cd lib
$ wget https://repo1.maven.org/maven2/mysql/mysql-connector-java/8.0.19/mysql-connector-java-8.0.19.jar

【2】设置使用 Nacos 注册中心

修改 conf/registry.conf 配置文件,设置使用 Nacos 注册中心。如下图所示:

【3】启动 TC Server

① 执行 nohup sh bin/seata-server.sh -p 18091 -n 1 & 命令,启动第一个 TC Server 在后台。

  • -p:Seata TC Server 监听的端口。
  • -n:Server node。在多个 TC Server 时,需区分各自节点,用于生成不同区间的 transactionId 事务编号,以免冲突。

② 执行 nohup sh bin/seata-server.sh -p 28091 -n 2 & 命令,启动第二个 TC Server 在后台。

③ 打开 Nacos 注册中心的控制台,我们可以看到有两个 Seata TC Server 示例。如下图所示:

Nacos 控制台

(3)Windows部署:

https://github.com/seata/seata/releases 下载并解压 Seata Server,按照官方文档进行配置,启动 TC(事务协调器)。

下面是下载链接地址:https://seata.apache.org/zh-cn/download/seata-server

随后解压后在其bin目录下使用cmd命令打开命令框,输入下面命令后即可等待seata启动:

seata-server.bat

在命令框中,我们可以看见 service listen port: 8091(Seata服务器监听8091端口)和 http://127.0.0.1:7091 (Seata的web界面地址,账号密码都是seata),如下图:

2.引入依赖:

为了方便各个微服务集成seata,我们需要把seata配置共享到nacos,因此模块不仅仅要引入seata依赖,还要引入nacos依赖:

  <!--统一配置管理-->
  <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
  </dependency>
  <!--读取bootstrap文件-->
  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-bootstrap</artifactId>
  </dependency>
  <!--seata-->
  <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
  </dependency>

3.加入配置来使用Seata:

首先Seata提供了事务分组,为了实现Seata服务器高可用(万一哪台Seata服务器炸了可快速切换):

seata:
  tx-service-group: default_tx_group # 这里是默认值default_tx_group,这个不写,见下面配置即可

 那么如何知道 Seata 服务器地址呢?这个时候我们的每一个项目一般需要引入一个 Seata 的配置文件,在每个微服务项目的 resource 中创建 file.conf 文件:

#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

transport {
  # tcp, unix-domain-socket
  type = "TCP"
  #NIO, NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  # the tm client batch send request enable
  enableTmClientBatchSendRequest = false
  # the rm client batch send request enable
  enableRmClientBatchSendRequest = true
   # the rm client rpc request timeout
  rpcRmRequestTimeout = 2000
  # the tm client rpc request timeout
  rpcTmRequestTimeout = 30000
  # the rm client rpc request timeout
  rpcRmRequestTimeout = 15000
  #thread factory for netty
  threadFactory {
    bossThreadPrefix = "NettyBoss"
    workerThreadPrefix = "NettyServerNIOWorker"
    serverExecutorThread-prefix = "NettyServerBizHandler"
    shareBossWorker = false
    clientSelectorThreadPrefix = "NettyClientSelector"
    clientSelectorThreadSize = 1
    clientWorkerThreadPrefix = "NettyClientWorkerThread"
    # netty boss thread size
    bossThreadSize = 1
    #auto default pin or 8
    workerThreadSize = "default"
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
  #transaction service group mapping
  vgroupMapping.default_tx_group = "default"
  #only support when registry.type=file, please don't set multiple addresses
  default.grouplist = "127.0.0.1:8091"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

client {
  rm {
    asyncCommitBufferLimit = 10000
    lock {
      retryInterval = 10
      retryTimes = 30
      retryPolicyBranchRollbackOnConflict = true
    }
    reportRetryCount = 5
    tableMetaCheckEnable = false
    tableMetaCheckerInterval = 60000
    reportSuccessEnable = false
    sagaBranchRegisterEnable = false
    sagaJsonParser = "fastjson"
    sagaRetryPersistModeUpdate = false
    sagaCompensatePersistModeUpdate = false
    tccActionInterceptorOrder = -2147482648 #Ordered.HIGHEST_PRECEDENCE + 1000
    sqlParserType = "druid"
    branchExecutionTimeoutXA = 60000
    connectionTwoPhaseHoldTimeoutXA = 10000
  }
  tm {
    commitRetryCount = 5
    rollbackRetryCount = 5
    defaultGlobalTransactionTimeout = 60000
    degradeCheck = false
    degradeCheckPeriod = 2000
    degradeCheckAllowTimes = 10
    interceptorOrder = -2147482648 #Ordered.HIGHEST_PRECEDENCE + 1000
  }
  undo {
    dataValidation = true
    onlyCareUpdateColumns = true
    logSerialization = "jackson"
    logTable = "undo_log"
    compress {
      enable = true
      # allow zip, gzip, deflater, lz4, bzip2, zstd default is zip
      type = zip
      # if rollback info size > threshold, then will be compress
      # allow k m g t
      threshold = 64k
    }
  }
  loadBalance {
      type = "XID"
      virtualNodes = 10
  }
}
log {
  exceptionRate = 100
}
tcc {
  fence {
    # tcc fence log table name
    logTableName = tcc_fence_log
    # tcc fence log clean period
    cleanPeriod = 1h
  }
}

而在上面的配置中,大多数都是默认配置,我们只需要在 file.conf 文件中写入如下配置即可:

service {
  #transaction service group mapping
  vgroupMapping.default_tx_group = "default"  # 事务分组
  #only support when registry.type=file, please don't set multiple addresses
  default.grouplist = "127.0.0.1:8091"  # TC协调者的PCP端口
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

随后我们在最大的入口内写入 Seata 提供的 @GlobalTransactional 全局事务注解即可,一但被该注解标注,在购买请求一开始就,Seata 就会控制全局事务 Business ,而其他的分支事务就会汇报给TC:

而 @GlobalTransactional 能够控制分布式事务是基于 Seata 底层的二阶提交协议

SpringCloud-第 2 页.drawio.png

 


(1)首先在nacos上添加一个共享的seata配置,命名为shared-seata.yaml

seata:
  registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
    type: nacos # 注册中心类型 nacos
    nacos:
      server-addr: 虚拟机IP地址:8848 # nacos地址
      namespace: "" # namespace,默认为空
      group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
      application: seata-server # seata服务名称
      username: nacos
      password: nacos
  tx-service-group: hmall # 事务组名称
  service:
    vgroup-mapping: # 事务组与tc集群的映射关系
      hmall: "default"
  • Data ID:公共配置的唯一标识(如:common-config.yaml)。
  • Group:可以使用默认的 DEFAULT_GROUP,或自定义一个组名(如:SHARED_GROUP)。
  • Namespace:选择命名空间(如:public,也可以自定义)。
  • 配置格式:建议选择 YAMLJSON 格式。

(2)改造模块,添加bootstrap.yaml

在 Spring Boot 项目中,bootstrap.yml 文件用于优先加载一些关键的配置信息,特别是涉及到外部配置中心服务注册等场景。与 application.yml 相比,bootstrap.yml 的加载优先级更高,因此一些必须在应用程序初始化前获取的配置需要写在 bootstrap.yml 中。 

spring:
  application:
    name: trade-service # 服务名称
  profiles:
    active: dev
  cloud:
    nacos:
      server-addr: 虚拟机IP地址 # nacos地址
      config:
        file-extension: yaml # 文件后缀名
        shared-configs: # 共享配置
          - dataId: shared-jdbc.yaml # 共享mybatis配置
          - dataId: shared-log.yaml # 共享日志配置
          - dataId: shared-swagger.yaml # 共享日志配置
          - dataId: shared-seata.yaml # 共享seata配置

bootstrap.yml 中启用了 refresh: true,无需重启服务,配置会自动刷新。 

(3)改造application.yaml文件:

server:
  port: 8085
feign:
  okhttp:
    enabled: true # 开启OKHttp连接池支持
  sentinel:
    enabled: true # 开启Feign对Sentinel的整合
hm:
  swagger:
    title: 交易服务接口文档
    package: com.hmall.trade.controller
  db:
    database: hm-trade

三.XA模式:

XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。

1.处理流程:

一阶段:

  • 事务协调者通知每个事务参与者执行本地事务

  • 本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁

二阶段:

  • 事务协调者基于一阶段的报告来判断下一步操作

  • 如果一阶段都成功,则通知所有事务参与者,提交事务

  • 如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务

2.注解使用: 

首先,我们要在配置文件中指定要采用的分布式事务模式。我们可以在Nacos中的共享shared-seata.yaml配置文件中设置:

seata:
  data-source-proxy-mode: XA

使用 @GlobalTransactional 注解全局事务:

代码中使用了 @GlobalTransactional 注解来标注全局事务,Seata 会协调参与者在两阶段提交协议下完成操作。你还可以根据业务需求调整数据库和注册中心的配置,以确保系统的高可用性。 

使用 @Transactional 来支持分布式事务的回滚:

@Transactional 是 Spring 提供的事务管理注解,用于声明式事务。如果事务内的某个操作抛出异常,事务将自动回滚。

对比项@Transactional@GlobalTransactional
适用范围单个服务或数据库中的事务多个服务或数据库之间的分布式事务
回滚机制只影响当前服务或数据库通过 Seata 协调多个数据库的回滚
异常处理默认只回滚运行时异常 (RuntimeException)可以指定任何异常回滚 (rollbackFor)
事务协调者Spring 事务管理器Seata TC (Transaction Coordinator)
嵌套事务支持支持嵌套事务,但需配置不支持嵌套事务,每个事务独立
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

记得开心一点嘛

您的打赏是对我最大的鼓励与期待

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值