SpringCloud多数据源接入Seata和ShardingJDBC最佳实践

SEATA整合sharding-jdbc 本文使用的初始源代码来自于:seata-samples/springcloud-seata-sharding-jdbc-mybatis-plus-samples at master · seata/seata-samples · GitHub 但是在运行上面代码过程中,发现老是运行失败,出现各种各样的错误,最后发现是因为配置的原因,初始代码中给的配置信息不全,这里给出完整的配置信息 1.seata-order- sample模块 (1). application.properties #订单微服务 阅读详情

一、前言

该方案基于seata官方示例进行修改,整合了spring-cloud-alibaba + seata + sharding-jdbc + mybatis + 多数据源,去除了mybatis-plus、sharding-transaction-base-seata-at依赖,采用手动配置seata和sharding-jdbc数据源的方式进行整合,通过@GlobalTransactional进行分布式事务的使用,和分库分表前的seata使用上没有区别。
注意:sharding-jdbc不同版本之间配置和设计差异较大,请合理选择sharding-jdbc版本

seata:1.4.2
sharding-jdbc:4.1.1
spring-boot:2.3.5
spring-cloud:Hoxton.SR9
spring-cloud-alibaba:2.2.5.RELEASE
原示例源码:https://github.com/seata/seata-samples/tree/master/springcloud-seata-sharding-jdbc-mybatis-plus-samples
本文档源码:https://github.com/jasonkung22/seata-samples/tree/master/springcloud-seata-sharding-jdbc-mybatis-plus-samples

二、原理剖析

1、剖析Seata分布式事务实现原理

数据源自动代理

seata开启自动代理数据源之后,每次注册dataSourceBean之后,SeataDataSourceBeanPostProcessor都会自动对数据源进行代理image.png

注册全局事务

当调用微服务A的@GlobalTransactional注解的方法时,会向TC注册全局事务,并得到全局事务ID(xid)
image.png

微服务间事务传递

当微服务A调用微服务B时,SeataRestTemplateInterceptor会将xid放入请求Header中传入微服务B,微服务B通过SeataHandlerInterceptor将请求Header中加载到当前线程中image.pngimage.png

是否添加 @Transactional 的区别
  1. 如果有@Transactional注解,会在本地事务提交时,向TC注册分支事务
  2. 如果没有@Transactional注解,会每次执行完SQL后,都向TC注册分支事务(如果一个方法中有3个数据操作,就会向TC注册3个分支事务)
提交或回滚全局事务
  1. 当微服务A的@GlobalTransactional方法正常执行完,TM会通知TC全局事务成功,TC再通知所有分支事务进行二阶段提交
  2. 当微服务A的@GlobalTransactional方法抛出异常时,TM会通知TC全局事务失败,TC再通知所有分支事务进行二阶段回滚

2、剖析ShardingJDBC实现原理

分片规则自动配置

在yaml中spring.shardingsphere.sharding配置分片规则后,会加载到SpringBootShardingRuleConfigurationProperties中image.png

数据源自动配置

在yaml中spring.shardingsphere.datasource配置数据源后,org.apache.shardingsphere.shardingjdbc.spring.boot.SpringBootConfiguration会自动创建ShardingDataSource,并注入分片规则等配置image.png

引入seata分布式事务(基于@ShardingTransactionType )

引入sharding-transaction-base-seata-at依赖后,SeataATShardingTransactionManager会自动对ShardingDataSource中的实际数据源创建seata数据源代理image.png

执行流程

当service调用dao时,根据SqlSessionFactory的MapperLocations配置识别到logicDateSource,然后根据分片规则找到实际数据源,完成以下流程图的SQL执行

三、设计思路

1、基于@ShardingTransactionType (5.4.0版本已弃用)

该方式在ShardingJDBC5.4.0版本前,为官方推荐,网上大多数也是基于此方案进行配置。但经过实测存在其它一些问题,不满足我们的应用场景

按照官方文档对接即可,不需要进行设计,对接中发现的问题如下:

  1. 不支持事务传播,如果微服务A和微服务B都引入了shardingJDBC,当微服务A调用微服务B时,微服务B的方法上面没有加@ShardingTransactionType,微服务B的不会加入到全局事务中
  2. 自动配置对多数据源支持不友好,需要自行创建ShardingDataSource,并注入到SqlSessionFactoryBean中
  3. @ShardingTransactionType和@GlobalTransactional不能混用,可能出现未知问题,比如空指针https://github.com/apache/shardingsphere/issues/22356

2、基于@GlobalTransactional(推荐)

由于seata和shardingJDBC本质上都是对数据源进行代理,我们只需要将shardingJDBC逻辑数据源下的每一个实际数据源当成多数据源,分片成功之后,就可以确定某一个数据源,再像分库分表前一样使用seata就可以了。总体执行流程:
分库分表前:mybatis > seataDataSource > druidDataSource > connection
分库分表后:mybatis > shardingDataSource > seataDataSource > druidDataSource > connection

  1. 为了配置多数据源,需要禁用spring默认的数据源自动配置
  2. 为了达到上诉的执行流程,需要自定义数据源和代理数据源,因此也需要禁用sharding、seata数据源自动配置
  3. 由于已经禁用了spring、seata、sharding的数据源自动配置,所以需要程序自定义数据源并进行管理。为了进行区分,将数据源定义在spring.database下

四、具体实现

1、基于@ShardingTransactionType

引入分布式事务seata依赖

微服务项目需要同时spring-cloud-starter-alibaba-seata依赖,不然微服务间调用时,xid不会进行传递

<dependency>
  <groupId>org.apache.shardingsphere</groupId>
  <artifactId>sharding-transaction-base-seata-at</artifactId>
  <version>${sharding-sphere.version}</version>
</dependency>
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
添加seata.config配置

需要将该配置放在classpath下

client {
    application.id = order-server
    transaction.service.group = seata-group
}
关闭seata的数据源自动代理
seata:
  enable-auto-data-source-proxy: false
使用@ShardingTransactionType

在需要分布式事务的方法上,加上@ShardingTransactionType(TransactionType.BASE)就可以正常使用了(但是ShardingTransactionType缺少事务的传播机制,需要注意加@ShardingTransactionType注解)

2、基于@GlobalTransactional

关闭seata自动代理数据源
seata:
  enable-auto-data-source-proxy: false
排除spring、shardingJDBC数据源自动配置
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, SpringBootConfiguration.class})
自定义shardingJDBC自动配置
@Configuration
@ComponentScan("org.apache.shardingsphere.spring.boot.converter")
@EnableConfigurationProperties({
        SpringBootShardingRuleConfigurationProperties.class,
        SpringBootMasterSlaveRuleConfigurationProperties.class, SpringBootEncryptRuleConfigurationProperties.class,
        SpringBootPropertiesConfigurationProperties.class, SpringBootShadowRuleConfigurationProperties.class})
@ConditionalOnProperty(prefix = "spring.shardingsphere", name = "enabled", havingValue = "true", matchIfMissing = true)
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
@RequiredArgsConstructor
public class CustomShardingSphereAutoConfiguration {


    /**
     * Create transaction type scanner.
     *
     * @return transaction type scanner
     */
    @Bean
    public ShardingTransactionTypeScanner transactionTypeScanner() {
        return new ShardingTransactionTypeScanner();
    }
}
配置多数据源

在yaml中spring.database下配置数据源

spring:
  database:
    ds0:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://192.168.0.7:3308/seata_order_0?serverTimezone=UTC&characterEncoding=utf8
      username: root
      password: 123456
      initial-size: 10
      max-active: 15
      min-idle: 10
      max-wait: 60000
    ds1:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://192.168.0.7:3308/seata_order_1?serverTimezone=UTC&characterEncoding=utf8
      username: root
      password: 123456
      initial-size: 10
      max-active: 15
      min-idle: 10
      max-wait: 60000
配置分片规则

在yaml中spring.shardingsphere下配置分片规则等配置(该方案适用于一个微服务只有一个shardingDataSource,如果需要有多个,也需要自定义分片规则参数)

spring:
  shardingsphere:
    props:
      sql:
        show: true
    sharding:
      default-data-source-name: ds0
      tables:
        order_info:
          actual-data-nodes: ds$->{0..1}.order_info_$->{0..2}
          database-strategy:
            inline:
              algorithm-expression: ds$->{id % 2}
              sharding-column: id
          table-strategy:
            inline:
              algorithm-expression: order_info_$->{id % 3}
              sharding-column: id
注册并代理数据源

自定义druidDataSource、seataDataSource、shardingDataSource并注入到SqlSessionFactory和TransactionManager里面

@Configuration
@MapperScan(basePackages = "io.seata.order.mapper", sqlSessionFactoryRef = "dsSqlSessionFactory")
@RequiredArgsConstructor
public class DataSourceConfig {

    private final SpringBootShardingRuleConfigurationProperties shardingRule;

    private final SpringBootPropertiesConfigurationProperties props;

    @Bean(name = "ds0DataSource")
    @ConfigurationProperties(prefix = "spring.database.ds0")
    public DataSource ds0DataSource() {
        DruidDataSource druidDataSource = DruidDataSourceBuilder.create().build();
        druidDataSource.setTestWhileIdle(true);
        druidDataSource.setValidationQuery("SELECT 1");
        return druidDataSource;
    }

    @Bean(name = "ds1DataSource")
    @ConfigurationProperties(prefix = "spring.database.ds1")
    public DataSource ds1DataSource() {
        DruidDataSource druidDataSource = DruidDataSourceBuilder.create().build();
        druidDataSource.setTestWhileIdle(true);
        druidDataSource.setValidationQuery("SELECT 1");
        return druidDataSource;
    }


    @Bean(name = "ds0SeataDatasource")
    public DataSourceProxy ds0SeataDatasource(@Qualifier("ds0DataSource") DataSource ds0DataSource) {
        return new DataSourceProxy(ds0DataSource);
    }


    @Bean(name = "ds1SeataDatasource")
    public DataSourceProxy ds1SeataDatasource(@Qualifier("ds1DataSource") DataSource ds1DataSource) {
        return new DataSourceProxy(ds1DataSource);
    }


    @Bean(name = "dsShardingDataSource")
    @Conditional(ShardingRuleCondition.class)
    public DataSource dsShardingDataSource(@Qualifier("ds0SeataDatasource") DataSource ds0SeataDatasource,
            @Qualifier("ds1SeataDatasource") DataSource ds1SeataDatasource) throws SQLException {
        return ShardingDataSourceFactory.createDataSource(getDataSourceMap(ds0SeataDatasource, ds1SeataDatasource),
                new ShardingRuleConfigurationYamlSwapper().swap(shardingRule), props.getProps());
    }

    @Bean(name = "dsSqlSessionFactory")
    public SqlSessionFactory logSqlSessionFactory(
            @Qualifier("dsShardingDataSource") DataSource dsShardingDataSource)
            throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dsShardingDataSource);
        bean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/ds/*.xml"));
        bean.setVfs(SpringBootVFS.class);
        return bean.getObject();
    }

    @Bean(name = "dsSqlSessionTemplate")
    public SqlSessionTemplate logSqlSessionTemplate(
            @Qualifier("dsSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean(name = "dsTransactionManager")
    public DataSourceTransactionManager controlLogTransactionManager(
            @Qualifier("dsShardingDataSource") DataSource dsShardingDataSource) {
        return new DataSourceTransactionManager(dsShardingDataSource);
    }

    public Map<String, DataSource> getDataSourceMap(DataSource controlLogDataSource, DataSource controlLog2DataSource) {
        Map<String,DataSource> dataSourceMap = new HashMap<>();
        dataSourceMap.put("ds0", controlLogDataSource);
        dataSourceMap.put("ds1", controlLog2DataSource);
        return dataSourceMap;
    }
}
多数据源配置方式和上面一样,源码中有所体现

五、参考资料

Seata官方博客-Seata数据源代理解析

布式事务Seata和分库分表sharding-sphere的整合 本篇将介绍,如何进行seata1.2.0、sharding-sphere4.1.0和dubbo2.7.5 的整合,以及使用nacos作为我们的配置中心和注册中心。如果你还是一个初学者,先建议学习一下,陈建斌的七步带你集成Seata 1.2 高可用搭建,这篇文章清楚的阐述了初学者容易遇到的5个问题,并且都提供完整的解决思路。nacos下载:https://github.com/alibaba/nacos/releases/tag/1.2.1Nacos 快速入门:https://nacos.io/en-us/d 阅读详情

相关推荐

基于springboot 2.2.2+sharding+seata实现分布式事务

分布式事务,是在微服务环境下一个很重要的支持框架。看了很多博文都无法能很好的把分布式事务有效的解决,要么在搭建的过程中遇到各种错误,要么就是根本搭建不起来,要么就是没有良好的解决方案。 本文使用sharding和seata集成,进行分布式事务管理,涉及相关技术框架pring boot 2.2.2、mybatis2.1.3、druid1.1.10、sharding4.1.0、mysql5.7、idgenerator1.4.2、eureka2.2.1、openfeign2.2.1、seata1.2

砌墙民工的博客 2140

JAVA Spring Cloud框架中实现多数据源配置

**业务描述:**在微服务搭建中经常会使用到多数据库情形这个时候,多数据源的切换将显得尤为重要下面是以Mysql数据库为数据源来实现的多数据源的切换方案之一。 实现过程: **1、**编写SpringCloud服务项目的配置文件,通常时修改项目中resources包下的bootstrap.yml文件(PS:也有可能是application.yml文件)这里使用yml文件格式来编写配置。 spring: #MySql数据源 datasource: #test1数据库配置

qq_38470695的博客 6086

Springcloud+Mybatis使用多数据源的四种方式

Springcloud+Mybatis使用多数据源的四种方式 前段时间在做会员中心和中间件系统开发时,多次碰到多数据源配置问题,主要用到分包方式、参数化切换、注解+AOP、动态添加 这四种方式。这里做一下总结,分享下使用心得以及踩过的坑。 分包方式 数据源配置文件 在yml中,配置两个数据源,id分别为master和s1。 spring: datasource: master: jdbcUrl: jdbc:mysql://192.168.xxx.xxx:xxxx/db1?

Java6888的博客 1645

SEATA整合sharding-jdbc思路

​ 这里主要是参考了ShardingSphere官网的整合思路 ​参考连接:https://shardingsphere.apache.org/document/legacy/4.x/document/cn/features/transaction/principle/base-transaction-seata/ ​ 具体内容如下: ​ 整合Seata AT事务时,需要把TM,RM,TC的模型融入到ShardingSphere 分布式事务的SPI的生态中。在数据库资源上,Seata通过对接DataSour

u010797364的博客 4259

抖音直播参数逆向实战:手把手教你用Python获取ttwid和x-bogus(附完整代码)

本文深入解析抖音直播接口的反爬机制,重点剖析核心防护参数ttwid和x-bogus的生成原理与作用。通过Python实战,手把手教你逆向获取ttwid、webid等关键参数,并构建稳定的数据采集方案,为开发者提供企业级数据获取能力。

weixin_29196891的博客 1043

【YOLO 系列】基于YOLOv8的工业零件表面缺陷智能检测系统【python源码+Pyqt5界面+数据集+训练代码】

本文详细介绍了如何基于YOLOv8深度学习模型和PyQt5图形界面,构建一套完整的工业零件表面缺陷智能检测系统。内容涵盖从环境搭建、数据集标注、模型训练调参到最终封装成可执行桌面软件的全流程,并深入解析了YOLOv8在工业视觉场景中的技术优势与落地优化策略。

ol78901234的博客 400

SpringCloud(16) 整合Seata+多数据源+sharding-jdbc

springcloud整合seata+多数据源+sharding-jdbc

郑清 3792

从C到汇编:单片机汇编语言核心概念迁移指南

本文详细介绍了从C语言迁移到单片机汇编语言的核心概念与实践指南,特别适合嵌入式开发者。文章深入探讨了汇编语言在性能优化、时序控制等方面的独特优势,并提供了变量存储、函数实现、流程控制等关键概念的对应关系,帮助开发者高效掌握汇编编程技巧。

weixin_31887307的博客 346

seata整合多数据源

seata整合多数据源一、背景二、整合步骤1、seata server的搭建2、引入数据源切换组件3、引入seata组件4、配置多数据源5、关闭seata自己默认的数据源代理6、配置seata事物分组7、业务库创建undo_log表8、xid的传递9、代码中使用数据源切换10、业务方法开启分布式事物三、注意事项四、完整代码 一、背景 在这篇文章中,我们使用Seata整合一下多数据源的场景。多数据源切换的功能我们使用dynamic-datasource-spring-boot-starter来完成,并且这个组

huan的学习记录 2704

攻防世界-Reverse-Game

攻防世界,game,逆向

wuh2333的博客 775

修改yolov8使支持单通道图像输入

默认支持BGR三通道图像输入,但是在很多工业场景下,存在很多灰度图像的场景,因此对yolov8代码做一些修改以支持单通道图像输入,满足项目实际需求。训练时,只需要在 data 的 yaml 文件中,添加 ch=1 即可。

mingo_敏 5779

Shardingsphere-jdbc整合Feign、Seata AT 模式实现分布式事务的解决方案

springboot整合shardingsphere-jdbc+seataAT实现分库分别和分布式事务

qq_42703153的博客 3300

大模型系列——Ollama简明教程

在这篇文章中,我们将了解如何使用 Ollama、如何在 Ollama 中创建自己的模型 以及如何使用 Ollama 构建聊天机器人。这是深入研究 Ollama 的第一部分,以及我所学到的有关本地 LLM 的知识以及如何将它们用于基于推理的应用程序。在这篇文章中,你将了解——

2401_84052244的博客 1万+

Seata 2.x 系列【11】多数据源分布式事务

在之前的案例中,我们在Spring Cloud微服务场景下使用Seata解决了分布式事务问题,在单体架构中,单个服务也可能存在跨库导致的分布式事务问题,例如下图中,因为分库导致下单请求需要连接多个数据库进行操作:

记录知识、锤炼自我 2311

UFS总体介绍及FPGA开发

通过实现UFS的主控制器、设备控制器等组件,我们可以在FPGA上进行UFS开发,并实现高速数据传输和低功耗特性的存储解决方案。物理层负责传输物理信号,数据链路层提供数据包的传输和错误检测功能,传输层处理数据包的分段、重组和重传,逻辑层管理设备和主控制器之间的命令和数据传输,文件系统层负责文件管理和数据存储。在本文中,我们将介绍UFS的总体架构和特性,并探讨如何在FPGA上进行UFS开发。这些组件的实现涉及到UFS协议的具体细节和硬件平台的特性,因此需要参考UFS协议规范和FPGA开发平台的相关文档。

PodProgram的博客 2135

详细介绍用MATLAB实现基于A*算法的路径规划(附完整的代码,代码逐行进行解释)(一)--------A*算法简介和环境的创建

   本系列文章主要介绍基于A*算法的路径规划的实现,并使用MATLAB进行仿真演示。    一、 A*算法简介       A*(A-Star)算法是一种静态路网中求解最短路径最有效的直接搜索方法,也是解决许多搜索问题的有效算法。算法中的距离估算值与实际值越接近,最终搜索速度越快。      公式表示为: f(n)=g(n)+h(n),其中, f(n) 是从初始状态经由状态n到目标状态的代价估计,g(n) 是在状态空间中从初始状态到状态n的实际代价,h(n) 是从状态n到目标状态的最佳路径的估计代价。

慕羽★的博客 7万+

SpringBoot集成Shardingjdbc+seata AT模式

SpringBoot集成shardingjdbc+seata-AT,分布式事务案例

qq_41563912的博客 5403

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

多数据源下Seata分布式事务出现的问题和解决方法

推荐使用AT模式 整体机制 两阶段提交协议的演变: 一阶段:业务数据和回滚日志记录在同一个本地事务中提交 (本地数据库先保存,并向undo_log表写入日志),释放本地锁和连接资源。 二阶段: 提交异步化,非常快速地完成。 回滚通过一阶段的回滚日志进行反向补偿(回滚时-读取undo_log表回滚回初始状态) ...

分享技术,享受生活 1万+

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

Seata整合Sharding-JDBC后分支事务回滚失败原因分析(高质量)

大家好,我是爱读源码的大都督,最近在做seata的升级改造,由于项目中既用到了seata,也用了sharding-jdbc,并且之前同事对他们进行改造,为了知道改造的原因,我做了一些测试,发现seata和sharding-jdbc同时使用时确实会出现问题,所以写这篇文章来记录一下,并分享给有需要的朋友。如果时间不够,并且对seata和sharding-jdbc的原理有一定了解的话,可以直接看最后的总结。

1点25的博客 2505

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

91. shardingshere5.2.1 + seata1.5.2实现AT模式的分布式事务

shardingshere5.2.1 + seata1.5.2实现AT模式的分布式事务

大头鱼 2876

动态多数据源使用seata实现分布式事务

一、背景 目前公司很多应用因为历史原因,一个应用访问多个数据库进行插入和更新操作,这就可能产生数据一致性问题,同时应用如果跨服务的调用也可能会产生事务问题。 目前应用是采用dynamic-datasource-spring-boot-starter做多数据源控制的。而seata是一款开源的分布式事务框架。我们了解到dynamic-datasource-spring-boot-starter的新版本已经支持基于seata的分布式事务了,而官网的例子基本都是标准的单数据源的整合,下面我们分别对dynam..

IT巅峰技术专栏 4745

Sharding-JDBC--分库分表+seata

本文以实例整合sharding-jdbc-spring-boot-starter:4.1.1、spring-cloud-starter-alibaba-seata:2.2.3.RELEASE、seata-spring-boot-starter:1.4.1、mybatis-plus-boot-starter:3.4.1的动态数据源

IT利刃出鞘的博客 2661

spring 项目 配置多数据源(已上生产,亲测有效)

🌸 修改配置文件,添加数据源配置 🌸 修改application.yaml spring: datasource: type: com.alibaba.druid.pool.DruidDataSource druid: db1: enable: true driver-class-name: com.mysql.cj.jdbc.Driver max-active: 100 min-idle: 100

Chen_chenjiasheng的博客 4060

Seata 实现多数据源事务

来源:https://github.com/baomidou/dynamic-datasource-spring-boot-starter/wiki/Integration-With-Seata Integration With Seata 小锅盖 edited this page2 days ago·3 revisions 简介 此工程为多数据源集成druid+seata+mybatisPlus的版本。 seata 官方Github地址:https://github.com/seata/...

攻城专栏 4354

Sharding-jdbc + Seata + Nacos整合

Sharding-jdbc、Seata、Nacos整合

王谷雨的博客 991

『征文精选』ShardingSphere-Proxy:Base 事务基于 Seata 验证

中文社区征文活动启动一周啦,我们陆陆续续收到了很多同学的优秀作品,今天为大家推荐其中的一篇,来自【ID:wsm12138】同学的实践心得。

SphereEX的博客 1139
上一篇: Spring boot升级为Spring cloud配置修改(服务注册和服务发现简单案例)
下一篇: 分布式定时任务PowerJob执行流程分析
AJiong93
博客等级 码龄10年 95粉丝 11原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值