解决SpringBoot项目整合Sharding-JDBC后启动慢的问题

一、问题描述

线上某一项目以jar包的方式发布,需要健康检查,若15次健康检查页面状态均为非200则健康检查失败,就会导致tomcat服务加载失败。前几天版本迭代,发布该项目时,因最后一次健康检查的时间比启动完成时早,所以认为启动失败,导致该项目未发布成功。

二、分析定位

项目以war包方式启动和jar包方式启动在开启8080端口的时间点上不一样。以war包的方式启动是在应用启动时就开启8080端口,此时应用还没启动成功,虽然健康检查接口访问不了,但因为端口是开的,所以Rundeck访问健康检查接口时会卡住,直到自行超时,所以整个健康检查的过程会被应用启动成功前的超时时间拉长。而以jar的方式启动,在应用启动成功之后才开启8080端口,在应用没启动成功前,Rundeck访问健康检查接口会直接报地址无法访问,一旦应用启动时间稍长,就会很快把Rundeck健康检查重试次数耗完,之后就认为启动失败了。

由于该项目整合了ShardingSphere-JDBC,并且有多张表需要分库分表,启动时会加载一些元数据、规则等会导致启动变慢所以引发Rundeck超时。

解决方法:

  1. 可以修改Rundeck启动超时时间。以上例为例,之前Rundeck超时时间是1分钟,可以修改为2分钟。

  1. 优化Sharding-JDBC启动流程。

方法一没什么好说的。下面说下方法二,不仅能解决问题,同时也能让我们在本地启动项目时更快些,方便我们自己调试。

三、背景

SringBoot版本:2.3.3.RELEASE

ShardingSphere版本:5.0.0

数据源连接池:Druid

数据库数为5,逻辑库名分别为dbm,db0,db1,db2,db3。其中dbm中存放单表(即不分库分表的表),db0~db3库用于分库。每个需要分库分表的表,按4库50表根据自定义分库分表算法进行水平切分。该项目单表数量为11,需要进行分库分表的表的数量为8。

配置文件大致如下:

# 数据源参数配置
initialSize=5
minIdle=5
maxIdle=100
maxActive=20
maxWait=60000
timeBetweenEvictionRunsMillis=60000
minEvictableIdleTimeMillis=300000
# Sharding Jdbc配置
# 分库的数量(注意:需要排除主库)
databaseSize=4
# 分表的数量
tableSize=50
# dbm为主库,db0,db1,db2,db3
spring.shardingsphere.datasource.names=dbm,db0,db1,db2,db3
# 配置主库
spring.shardingsphere.datasource.dbm.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.dbm.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.dbm.url=jdbc:mysql://ip:3306/rt_warehouse_inventory?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false&tinyInt1isBit=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
spring.shardingsphere.datasource.dbm.username=username
spring.shardingsphere.datasource.dbm.password=password
spring.shardingsphere.datasource.dbm.initialSize=${initialSize}
spring.shardingsphere.datasource.dbm.minIdle=${minIdle}
spring.shardingsphere.datasource.dbm.maxActive=${maxActive}
spring.shardingsphere.datasource.dbm.maxWait=${maxWait}
spring.shardingsphere.datasource.dbm.validationQuery=SELECT 1 FROM DUAL
spring.shardingsphere.datasource.dbm.timeBetweenEvictionRunsMillis=${timeBetweenEvictionRunsMillis}
spring.shardingsphere.datasource.dbm.minEvictableIdleTimeMillis=${minEvictableIdleTimeMillis}
# 配置db0
spring.shardingsphere.datasource.db0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.db0.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.db0.url=jdbc:mysql://ip:3306/rt_warehouse_inventory_00?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false&tinyInt1isBit=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
spring.shardingsphere.datasource.db0.username=username
spring.shardingsphere.datasource.db0.password=password
spring.shardingsphere.datasource.db0.initialSize=${initialSize}
spring.shardingsphere.datasource.db0.minIdle=${minIdle}
spring.shardingsphere.datasource.db0.maxActive=${maxActive}
spring.shardingsphere.datasource.db0.maxWait=${maxWait}
spring.shardingsphere.datasource.db0.validationQuery=SELECT 1 FROM DUAL
spring.shardingsphere.datasource.db0.timeBetweenEvictionRunsMillis=${timeBetweenEvictionRunsMillis}
spring.shardingsphere.datasource.db0.minEvictableIdleTimeMillis=${minEvictableIdleTimeMillis}
# 配置db1
spring.shardingsphere.datasource.db1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.db1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.db1.url=jdbc:mysql://ip:3306/rt_warehouse_inventory_01?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false&tinyInt1isBit=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
spring.shardingsphere.datasource.db1.username=username
spring.shardingsphere.datasource.db1.password=password
spring.shardingsphere.datasource.db1.initialSize=${initialSize}
spring.shardingsphere.datasource.db1.minIdle=${minIdle}
spring.shardingsphere.datasource.db1.maxActive=${maxActive}
spring.shardingsphere.datasource.db1.maxWait=${maxWait}
spring.shardingsphere.datasource.db1.validationQuery=SELECT 1 FROM DUAL
spring.shardingsphere.datasource.db1.timeBetweenEvictionRunsMillis=${timeBetweenEvictionRunsMillis}
spring.shardingsphere.datasource.db1.minEvictableIdleTimeMillis=${minEvictableIdleTimeMillis}
# 配置db2
spring.shardingsphere.datasource.db2.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.db2.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.db2.url=jdbc:mysql://ip:3306/rt_warehouse_inventory_02?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false&tinyInt1isBit=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
spring.shardingsphere.datasource.db2.username=username
spring.shardingsphere.datasource.db2.password=password
spring.shardingsphere.datasource.db2.initialSize=${initialSize}
spring.shardingsphere.datasource.db2.minIdle=${minIdle}
spring.shardingsphere.datasource.db2.maxActive=${maxActive}
spring.shardingsphere.datasource.db2.maxWait=${maxWait}
spring.shardingsphere.datasource.db2.validationQuery=SELECT 1 FROM DUAL
spring.shardingsphere.datasource.db2.timeBetweenEvictionRunsMillis=${timeBetweenEvictionRunsMillis}
spring.shardingsphere.datasource.db2.minEvictableIdleTimeMillis=${minEvictableIdleTimeMillis}
# 配置db3
spring.shardingsphere.datasource.db3.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.db3.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.db3.url=jdbc:mysql://ip:3306/rt_warehouse_inventory_03?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false&tinyInt1isBit=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
spring.shardingsphere.datasource.db3.username=username
spring.shardingsphere.datasource.db3.password=password
spring.shardingsphere.datasource.db3.initialSize=${initialSize}
spring.shardingsphere.datasource.db3.minIdle=${minIdle}
spring.shardingsphere.datasource.db3.maxActive=${maxActive}
spring.shardingsphere.datasource.db3.maxWait=${maxWait}
spring.shardingsphere.datasource.db3.validationQuery=SELECT 1 FROM DUAL
spring.shardingsphere.datasource.db3.timeBetweenEvictionRunsMillis=${timeBetweenEvictionRunsMillis}
spring.shardingsphere.datasource.db3.minEvictableIdleTimeMillis=${minEvictableIdleTimeMillis}
# 分库配置
spring.shardingsphere.rules.sharding.default-database-strategy.standard.sharding-column=store_no
spring.shardingsphere.rules.sharding.default-database-strategy.standard.sharding-algorithm-name=preciseShardingDatabaseAlgorithm
# 分表配置
# test1表配置
spring.shardingsphere.rules.sharding.tables.test1.actual-data-nodes=db$->{0..3}.test1_0$->{0..9},db$->{0..3}.test1_$->{10..49}
spring.shardingsphere.rules.sharding.tables.test1.table-strategy.standard.sharding-column=store_no
spring.shardingsphere.rules.sharding.tables.test1.table-strategy.standard.sharding-algorithm-name=preciseShardingTableAlgorithm
# test2表配置
spring.shardingsphere.rules.sharding.tables.test2.actual-data-nodes=db$->{0..3}.test2_0$->{0..9},db$->{0..3}.test2_$->{10..49}
spring.shardingsphere.rules.sharding.tables.test2.table-strategy.standard.sharding-column=store_no
spring.shardingsphere.rules.sharding.tables.test2.table-strategy.standard.sharding-algorithm-name=preciseShardingTableAlgorithm
# test3表配置
spring.shardingsphere.rules.sharding.tables.test3.actual-data-nodes=db$->{0..3}.test3_0$->{0..9},db$->{0..3}.test3_$->{10..49}
spring.shardingsphere.rules.sharding.tables.test3.table-strategy.standard.sharding-column=store_no
spring.shardingsphere.rules.sharding.tables.test3.table-strategy.standard.sharding-algorithm-name=preciseShardingTableAlgorithm
# test4表配置
spring.shardingsphere.rules.sharding.tables.test4.actual-data-nodes=db$->{0..3}.test4_0$->{0..9},db$->{0..3}.test4_$->{10..49}
spring.shardingsphere.rules.sharding.tables.test4.table-strategy.standard.sharding-column=store_no
spring.shardingsphere.rules.sharding.tables.test4.table-strategy.standard.sharding-algorithm-name=preciseShardingTableAlgorithm
# test5表配置
spring.shardingsphere.rules.sharding.tables.test5.actual-data-nodes=db$->{0..3}.test5_0$->{0..9},db$->{0..3}.test5_$->{10..49}
spring.shardingsphere.rules.sharding.tables.test5.table-strategy.standard.sharding-column=store_no
spring.shardingsphere.rules.sharding.tables.test5.table-strategy.standard.sharding-algorithm-name=preciseShardingTableAlgorithm
# test6表配置
spring.shardingsphere.rules.sharding.tables.test6.actual-data-nodes=db$->{0..3}.test6_0$->{0..9},db$->{0..3}.test6_$->{10..49}
spring.shardingsphere.rules.sharding.tables.test6.table-strategy.standard.sharding-column=store_no
spring.shardingsphere.rules.sharding.tables.test6.table-strategy.standard.sharding-algorithm-name=preciseShardingTableAlgorithm
# test7表配置
spring.shardingsphere.rules.sharding.tables.test7.actual-data-nodes=db$->{0..3}.test7_0$->{0..9},db$->{0..3}.test7_$->{10..49}
spring.shardingsphere.rules.sharding.tables.test7.table-strategy.standard.sharding-column=store_no
spring.shardingsphere.rules.sharding.tables.test7.table-strategy.standard.sharding-algorithm-name=preciseShardingTableAlgorithm
# test8表配置
spring.shardingsphere.rules.sharding.tables.test8.actual-data-nodes=db$->{0..3}.test8_0$->{0..9},db$->{0..3}.test8_$->{10..49}
spring.shardingsphere.rules.sharding.tables.test8.table-strategy.standard.sharding-column=store_no
spring.shardingsphere.rules.sharding.tables.test8.table-strategy.standard.sharding-algorithm-name=preciseShardingTableAlgorithm

# 打印分库分表日志
spring.shardingsphere.props.sql-show=true

本地测试十次启动应用,启动时间如下图:

启动次数

第一次

第二次

第三次

第四次

第五次

第六次

第七次

第八次

第九次

第十次

启动时间

29.625s

23.748s

24.299s

22.575s

24.167s

23.13s

23.73s

23.419s

22.91s

23.04s

四、优化方式

先来看下在启动时,ShardingSphere-JDBC如何加载单表规则。

首先在创建单表规则SingleTableRule时,通过getAggregateDataSourceMap方法获取所有数据源。以本例为例,即通过该方法将获取dbm,db0,db1,db2,db3数据源。然后通过单表数据节点加载器加载所有数据源中的单表。其中通过getExcludedTables(builtRules)方法获取所有数据分片的表,包括逻辑表名和真实表名。如下图:

然后循环遍历所有数据源,首先加载每个数据源的所有表名,然后判断改表是否是要数据分片的表,若不是,则即是单表,根据该单表名和数据源逻辑名创建单表数据节点。如前所述,对于dbm数据源,我们只存放单表,共11张表,通过SingleTableDataNodeLoader.loadAllTableNames方法即获得11张单表,循环遍历11次,并且这11张表不在excludedTables(需要数据分片的表名,本例共8个表需要数据分片,每个表水平切分50个真实表,加上逻辑表名,共408个表名,为test1,test2,test3,test4,test5,test6,test7,test8,test1_00~test1_19,test2_00~test2_19,test3_00~test3_19,test4_00~test4_19,test5_00~test5_19,test6_00~test6_19,test7_00~test7_19,test8_00~test8_19)中,根据这11个单表表名和dbm数据源创建11个单表数据节点,本次循环结束。对于db0数据源,共存放8 * 50 共400张真实表,表名为test1_00~test1_19,test2_00~test2_19,test3_00~test3_19,test4_00~test4_19,test5_00~test5_19,test6_00~test6_19,test7_00~test7_19,test8_00~test8_19。通过SingleTableDataNodeLoader.loadAllTableNames方法即获得上述400个表名,循环遍历400次,判断表名是否存在于excludedTables中,显然这400个表名是存在的。显而易见这400次循环遍历判断是多余的,因为单表只存在于dbm中,db0中只存在数据分片的表,且肯定存在于excludedTables中。对于db1、db2、db3同理。源码如下图:

通过上面分析可知,对于我这个项目的实际情况,单表只在dbm中,在加载单表时完全不用去db0~db3数据源中去加载。显然这块共四次加载所有表名即4*400=1600次循环判断是否是单表是多余的。若数据源增多或者需要数据分片的表增多,在上述多余的步骤上耗时也会变多,项目启动会变的更慢。可以通过修改getAggregateDataSourceMap方法,只获取dbm数据源来优化启动时间。

具体优化方式如下:

  1. 使用HikariCP数据源连接池替换Druid数据源连接池。

  1. 在应用里建一个和SingleTableRule类相同名称且路路径相同的文件,将下面内容拷贝进去,这样不会动到原jar包。下面内容就是将getAggregateDataSourceMap方法替换为getMasterDataSourceMap方法,只获取dbm数据源。

package org.apache.shardingsphere.singletable.rule;

import lombok.Getter;
import org.apache.shardingsphere.infra.config.properties.ConfigurationProperties;
import org.apache.shardingsphere.infra.database.type.DatabaseType;
import org.apache.shardingsphere.infra.datanode.DataNode;
import org.apache.shardingsphere.infra.route.context.RouteContext;
import org.apache.shardingsphere.infra.route.context.RouteUnit;
import org.apache.shardingsphere.infra.rule.ShardingSphereRule;
import org.apache.shardingsphere.infra.rule.identifier.scope.SchemaRule;
import org.apache.shardingsphere.infra.rule.identifier.type.DataNodeContainedRule;
import org.apache.shardingsphere.infra.rule.identifier.type.DataSourceContainedRule;
import org.apache.shardingsphere.infra.rule.identifier.type.MutableDataNodeRule;
import org.apache.shardingsphere.infra.rule.identifier.type.TableContainedRule;

import javax.sql.DataSource;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Single table rule.
 */
@Getter
public final class SingleTableRule implements SchemaRule, DataNodeContainedRule, TableContainedRule, MutableDataNodeRule {

    private final Collection<String> dataSourceNames;

    private final Map<String, SingleTableDataNode> singleTableDataNodes;

    public SingleTableRule(final DatabaseType databaseType, final Map<String, DataSource> dataSourceMap, final Collection<ShardingSphereRule> builtRules, final ConfigurationProperties props) {
        Map<String, DataSource> aggregateDataSourceMap = getMaterDataSourceMap(dataSourceMap);
        dataSourceNames = aggregateDataSourceMap.keySet();
        singleTableDataNodes = SingleTableDataNodeLoader.load(databaseType, aggregateDataSourceMap, getExcludedTables(builtRules), props);
    }

    // 只获取主数据库数据源
    private Map<String, DataSource> getMasterDataSourceMap(final Map<String, DataSource> dataSourceMap) {
        Map<String, DataSource> result = new LinkedHashMap<>(1);
        for (Entry<String, DataSource> entry : dataSourceMap.entrySet()) {
            String logicName = entry.getKey();
            if (logicName != null) {
                result.put(logicName,dataSourceMap.get(logicName));
            }
            break;
        }
        return result;
    }

    /**
     * Judge whether single tables are in same data source or not.
     *
     * @param singleTableNames single table names
     * @return whether single tables are in same data source or not
     */
    public boolean isSingleTablesInSameDataSource(final Collection<String> singleTableNames) {
        Set<String> dataSourceNames = singleTableNames.stream().map(singleTableDataNodes::get).filter(Objects::nonNull).map(SingleTableDataNode::getDataSourceName).collect(Collectors.toSet());
        return dataSourceNames.size() <= 1;
    }

    /**
     * Judge whether all tables are in same data source or not.
     *
     * @param routeContext route context
     * @param singleTableNames single table names
     * @return whether all tables are in same data source or not
     */
    public boolean isAllTablesInSameDataSource(final RouteContext routeContext, final Collection<String> singleTableNames) {
        if (!isSingleTablesInSameDataSource(singleTableNames)) {
            return false;
        }
        SingleTableDataNode dataNode = singleTableDataNodes.get(singleTableNames.iterator().next());
        for (RouteUnit each : routeContext.getRouteUnits()) {
            if (!each.getDataSourceMapper().getLogicName().equals(dataNode.getDataSourceName())) {
                return false;
            }
        }
        return true;
    }

    /**
     * Get sharding logic table names.
     *
     * @param logicTableNames logic table names
     * @return sharding logic table names
     */
    public Collection<String> getSingleTableNames(final Collection<String> logicTableNames) {
        return logicTableNames.stream().filter(singleTableDataNodes::containsKey).collect(Collectors.toCollection(LinkedList::new));
    }

    @Override
    public void addDataNode(final String tableName, final String dataSourceName) {
        if (dataSourceNames.contains(dataSourceName) && !singleTableDataNodes.containsKey(tableName)) {
            singleTableDataNodes.put(tableName, new SingleTableDataNode(tableName, dataSourceName));
        }
    }

    @Override
    public void dropDataNode(final String tableName) {
        singleTableDataNodes.remove(tableName);
    }

    private Collection<String> getExcludedTables(final Collection<ShardingSphereRule> rules) {
        return rules.stream().filter(each -> each instanceof DataNodeContainedRule).flatMap(each -> ((DataNodeContainedRule) each).getAllTables().stream()).collect(Collectors.toSet());
    }

    @Override
    public Map<String, Collection<DataNode>> getAllDataNodes() {
        return singleTableDataNodes.values().stream().map(each -> new DataNode(each.getDataSourceName(), each.getTableName()))
                .collect(Collectors.groupingBy(DataNode::getTableName, LinkedHashMap::new, Collectors.toCollection(LinkedList::new)));
    }

    @Override
    public Collection<String> getAllActualTables() {
        return Collections.emptyList();
    }

    @Override
    public Optional<String> findFirstActualTable(final String logicTable) {
        return Optional.empty();
    }

    @Override
    public boolean isNeedAccumulate(final Collection<String> tables) {
        return false;
    }

    @Override
    public Optional<String> findLogicTableByActualTable(final String actualTable) {
        return Optional.empty();
    }

    @Override
    public Optional<String> findActualTableByCatalog(final String catalog, final String logicTable) {
        return Optional.empty();
    }

    @Override
    public Collection<String> getAllTables() {
        return singleTableDataNodes.keySet();
    }

    @Override
    public Collection<String> getTables() {
        return singleTableDataNodes.keySet();
    }

    @Override
    public String getType() {
        return SingleTableRule.class.getSimpleName();
    }
}

优化后本地测试十次启动应用,启动时间如下图

启动次数

第一次

第二次

第三次

第四次

第五次

第六次

第七次

第八次

第九次

第十次

启动时间

15.231s

17.426s

16.395s

16.19s

17.029s

15.169s

15.025s

15.559s

14.975s

14.699s

通过优化前后本地启动时间对比可以看出应用启动明显快了些。

该方式适用于ShardingSphere版本:5.0.0和5.2.0版本,只在这两个正在使用的版本上验证了下,其他版本并未验证是否可行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
您好!使用Sharding-JDBC在Spring Boot项目中进行数据库分片是一个不错的选择。下面是一些步骤来集成Sharding-JDBC到Spring Boot项目中: 第一步,添加Sharding-JDBC依赖到项目的pom.xml文件中: ```xml <dependency> <groupId>io.shardingsphere</groupId> <artifactId>sharding-jdbc-spring-boot-starter</artifactId> <version>4.1.1</version> </dependency> ``` 第二步,配置Sharding-JDBC相关的数据源和规则。可以在application.properties或application.yml文件中进行配置,例如: ```yaml spring: shardingsphere: datasource: names: ds0, ds1 ds0: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/db0 username: root password: password ds1: driver-class-name: com.mysql.cj.jdbc.Driver jdbc-url: jdbc:mysql://localhost:3306/db1 username: root password: password sharding: tables: user: actual-data-nodes: ds${0..1}.user_${0..1} table-strategy: inline: sharding-column: id algorithm-expression: user_${id % 2} ``` 在上面的示例中,配置了两个数据源(ds0和ds1),并指定了分片规则:根据user表的id字段进行分片,将数据分散到ds0.user_0、ds0.user_1、ds1.user_0、ds1.user_1这四个表中。 第三步,使用Sharding-JDBC的数据源进行数据库操作。在需要使用数据库的地方,注入ShardingDataSource数据源,例如: ```java @Autowired private DataSource dataSource; public void queryData() { try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) { ResultSet resultSet = statement.executeQuery("SELECT * FROM user"); while (resultSet.next()) { // 处理查询结果 } } catch (SQLException e) { // 异常处理 } } ``` 以上是使用Sharding-JDBC在Spring Boot项目中进行数据库分片的基本步骤,您可以根据具体需求进行更详细的配置和使用。希望能对您有所帮助!如果还有其他问题,请随时提问。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

luffylv

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值