分布式事务框架以及解决方案

XA Transactions

在这里插入图片描述
https://dev.mysql.com/doc/refman/5.7/en/xa.html

全局事务涉及几个本身具有事务性的操作,但是所有操作必须作为一个组同时COMMIT或者ROLLBACK,从本质上讲,这将ACID属性扩展到上一个级别,以便可以将多个ACID事务作为具有ACID属性的全局操作的组成部分一起执行。

使用全局事务的应用程序涉及一个或多个资源管理器和一个事务管理器:

  • 资源管理器(RM)提供对事务性资源的访问。数据库服务器是一种资源管理器。必须能够提交或回滚RM管理的事务。
  • 事务管理器(TM)协调作为全局事务一部分的事务。它与处理每个事务的RM通信。全局事务中的单个事务是全局事务的 “分支”。全局事务及其分支由后面描述的命名方案标识。

执行全局事务的过程使用两阶段提交(2PC)。这是在执行全局事务的分支所执行的动作之后发生的。

  • 1、在第一阶段,准备所有分支。也就是说,TM告诉他们准备提交。通常,这意味着管理分支的每个RM在稳定的存储中记录分支的动作。分支指示它们是否能够执行此操作,并将这些结果用于第二阶段。
  • 2、在第二阶段,TM告诉RM是提交还是回滚。如果所有分支在准备就绪时都表示可以提交,则通知所有分支提交。如果任何分支在准备就绪时指示无法提交,则将告知所有分支回滚。
  • 在某些情况下,全局事务可能使用一阶段提交(1PC)。例如,当事务管理器发现全局事务仅包含一个事务资源(即单个分支)时,可以告知该资源同时准备和提交。

XA事务状态:

  • 使用XA START启动一个XA事务,并把它的 ACTIVE状态。

  • 对于ACTIVEXA事务,发出组成该事务的SQL语句,然后发出一条XA END语句。 XA END将交易置于 IDLE状态。

  • 对于IDLEXA事务,可以发出一个XA PREPARE语句或一个XA COMMIT … ONE PHASE语句:

    XA PREPARE将交易置于 PREPARED状态。此时的一条 XA RECOVER语句将事务的xid值包含在其输出中,因为 XA RECOVER列出了处于该PREPARED状态的所有XA事务。 
    XA COMMIT ... ONE PHASE准备并提交事务。xid未列出该 值, XA RECOVER因为事务终止。
    
  • 于PREPAREDXA事务,您可以发出一条XA COMMIT语句来提交和终止该事务,或者 XA ROLLBACK回滚并终止该事务。

    mysql> XA START 'xatest';
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> INSERT INTO mytable (i) VALUES(10);
    Query OK, 1 row affected (0.04 sec)
    
    mysql> XA END 'xatest';
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> XA PREPARE 'xatest';
    Query OK, 0 rows affected (0.00 sec)
    
    mysql> XA COMMIT 'xatest';
    Query OK, 0 rows affected (0.00 sec)
    

两阶段提交(2PC)

两阶段提交又称2PC(two-phase commit protocol),2pc是一个非常经典的强一致、中心化的原子提交协议。这里所说的中心化是指协议中有两类节点:一个是中心化协调者节点(coordinator)和N个参与者节点(participant)。

第一阶段(Prepare)

在这里插入图片描述

  • 事务询问:协调者 向所有的 参与者 发送事务预处理请求,称之为Prepare,并开始等待各 参与者 的响应。

  • 执行本地事务:各个 参与者 节点执行本地事务操作,但在执行完成后并不会真正提交数据库本地事务,而是先向 协调者 报告说:“我这边可以处理了/我这边不能处理”。.

  • 各参与者向协调者反馈事务询问的响应:如果 参与者 成功执行了事务操作,那么就反馈给协调者 Yes 响应,表示事务可以执行,如果没有 参与者 成功执行事务,那么就反馈给协调者 No 响应,表示事务不可以执行。

    第一阶段执行完后,会有两种可能。1、所有都返回Yes. 2、有一个或者多个返回No。

第二阶段(Commit Yes)

在这里插入图片描述

  • 所有的参与者反馈给协调者的信息都是Yes,那么就会执行事务提交,协调者 向 所有参与者 节点发出Commit请求.

  • 事务提交,参与者 收到Commit请求之后,就会正式执行本地事务Commit操作,并在完成提交之后释放整个事务执行期间占用的事务资源。

第二阶段(Commit No)

在这里插入图片描述

  • 发送回滚请求,协调者 向所有参与者节点发出 RoollBack 请求。

  • 事务回滚,参与者 接收到RoollBack请求后,会回滚本地事务。

2PC缺点

  • 性能问题。从流程上我们可以看得出,其最大缺点就在于它的执行过程中间,节点都处于阻塞状态。各个操作数据库的节点此时都占用着数据库资源,只有当所有节点准备完毕,事务协调者才会通知进行全局提交,参与者进行本地事务提交后才会释放资源。这样的过程会比较漫长,对性能影响比较大。
  • 协调者单点故障问题。事务协调者是整个XA模型的核心,一旦事务协调者节点挂掉,会导致参与者收不到提交或回滚的通知,从而导致参与者节点始终处于事务无法完成的中间状态。
    (1)协调者正常,参与者宕机	
    ​ 由于 协调者 无法收集到所有 参与者 的反馈,会陷入阻塞情况。	
    ​ 解决方案:引入超时机制,如果协调者在超过指定的时间还没有收到参与者的反馈,事务就失败,向所有节点发送终止事务请求。
    
    (2)协调者宕机,参与者正常	
    ​ 无论处于哪个阶段,由于协调者宕机,无法发送提交请求,所有处于执行了操作但是未提交状态的参与者都会陷入阻塞情况.	
    ​ 解决方案:引入协调者备份,同时协调者需记录操作日志.当检测到协调者宕机一段时间后,协调者备份取代协调者,并读取操作日志,向所有参与者询问状态。
    
    (3)协调者和参与者都宕机	
      a.发生在第一阶段: 因为第一阶段,所有参与者都没有真正执行commit,所以只需重新在剩余的参与者中重新选出一个协调者,新的协调者在重新执行第一阶段和第二阶段就可以了。
      b.发生在第二阶段 并且 挂了的参与者在挂掉之前没有收到协调者的指令。也就是上面的第4步挂了,这是可能协调者还没有发送第4步就挂了。这种情形下,新的协调者重新执行第一阶段和第二阶段操作。
      c.发生在第二阶段 并且 有部分参与者已经执行完commit操作。就好比这里订单服务A和支付服务B都收到协调者 发送的commit信息,开始真正执行本地事务commit,但突发情况,Acommit成功,B确挂了。这个时候目前来讲数据是不一致的。虽然这个时候可以再通过手段让他和协调者通信,再想办法把数据搞成一致的,但是,这段时间内他的数据状态已经是不一致的了! 2PC 无法解决这个问题。
    
  • 丢失消息导致的数据不一致问题。在第二个阶段,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就会导致节点间数据的不一致问题。

三阶段提交(3PC)

三阶段提交又称3PC,其在两阶段提交的基础上增加了CanCommit阶段,并引入了超时机制。一旦事务参与者迟迟没有收到协调者的Commit请求,就会自动进行本地commit,这样相对有效地解决了协调者单点故障的问题。

第一阶段 (CanCommit)

在这里插入图片描述

  • 事务询问 协调者 向 参与者 发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待 参与者 的响应。
  • 响应反馈 参与者 接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No

第二阶段 (PreCommit)

在这里插入图片描述

  • 执行事务预提交:
    • 发送事务预提交请求:协调者向所有参与者节点发出PreCommit的请求,并进入Prepared阶段。
    • 执行事务预提交操作:参与者接收到preCommit请求后,会执行事务操作,并将Undo和Redo信息记录到事务日志中。(执行但不提交)
    • 反馈事务执行的响应:如果参与者成功执行了事务操作,那么就会反馈给协调者Ack响应,同时等待最终的指令:提交(commit)或中止(abort)。
  • 中断事务:
    • 发送中断请求:协调者向所有参与者节点发出abort请求。
    • 无论是收到来自协调者的abort请求,或者是在等待协调者发送请求过程中出现超时,参与者都会中断事务。

第三阶段 (DoCommit)

在这里插入图片描述

  • 执行提交:
    • 发送提交请求:进入这一阶段,假设协调者处于正常工作状态,并且它接收到了来自所有参与者的Ack响应,那么它将从“预提交”状态转换到“提交”状态,并向所有的参与者发送doCommit请求。
    • 执行事务操作:参与者接收到doCommit请求后,会正式执行事务提交操作,并在完成提交之后释放在整个事务执行期间占用的事务资源。
    • 反馈事务提交结果:参与者在完成事务提交之后,向协调者发送Ack消息。
    • 完成事务:协调者接收到所有参与者反馈的Ack消息之后,完成事务。
  • 中断事务:任意一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。
    • 发送中断请求:协调者向所有的参与者节点发送abort请求。

    • 事务回滚:参与者接收到abort请求后,会利用其在阶段二中记录的Undo信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。

    • 反馈事务回滚结果:参与者在完成事务回滚之后,向协调者发送Ack消息。

    • 中断事务:协调者接收到所有参与者反馈的Ack消息后,中断事务。

    • 进入第三阶段,无论是协调者和参与者故障,最终都会导致参与者无法及时接收到来自协调者的doCommit或是abort请求,针对于这样的异常情况,参与者都会在等待超时之后,继续进行事务提交。

结论

  • 相比较2PC而言,3PC对于2PC都设置了超时时间,而2PC只有协调者才拥有超时机制。

  • 这个优化点,主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。

  • 另外,通过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的。

  • 以上就是3PC相对于2PC的一个提高(相对缓解了2PC中的前两个问题),但是3PC依然没有完全解决数据不一致的问题。

消息队列+事件表

在这里插入图片描述

处理流程

  • 1、调用支付系统,支付系统业务处理,更新状态出入流水记录;
  • 2、插入事件表,记录状态NEW;
  • 3、定时查询事件表记录【NEW】,然后更新记录状态为SEND,同时发送消息队列;
  • 4、消息消费者接收到消息,插入事件表,记录状态NEW;
  • 5、定时任务查询记录【NEW】;
  • 6、订单系统业务处理,如果成功,更新记录状态SUCCESS,手动确认ACK;如果失败,重新消费,消费次数达到6次以上,进入死信队列,补偿处理。

问题

  • 1、定时任务瓶颈;
  • 2、不适用数据量大的系统;

LCN( Lock、Confirm、Notify)

原理

LCN模式是通过代理Connection的方式实现对本地事务的操作,然后在由TxManager统一协调控制事务。当本地事务提交回滚或者关闭连接时将会执行假操作,该代理的连接将由LCN连接池管理。
https://github.com/codingapi/tx-lcn/
https://www.codingapi.com/docs/txlcn-preface/

特点

  • 该模式对代码的嵌入性为低。
  • 该模式仅限于本地存在连接对象且可通过连接对象控制事务的模块。
  • 该模式下的事务提交与回滚是由本地事务方控制,对于数据一致性上有较高的保障。
  • 该模式缺陷在于代理的连接需要随事务发起方一共释放连接,增加了连接占用的时间。

原理图:

在这里插入图片描述
TX-LCN由两大模块组成, TxClient、TxManager,TxClient作为模块的依赖框架,提供TX-LCN的标准支持,TxManager作为分布式事务的控制方。事务发起方或者参与方都由TxClient端来控制。

  • pom.xml

    <dependency>
        <groupId>com.codingapi.txlcn</groupId>
        <artifactId>txlcn-tm</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
        <!-- https://mvnrepository.com/artifact/com.codingapi.txlcn/txlcn-tc -->
    <dependency>
        <groupId>com.codingapi.txlcn</groupId>
        <artifactId>txlcn-tc</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/com.codingapi.txlcn/txlcn-txmsg-netty -->
    <dependency>
        <groupId>com.codingapi.txlcn</groupId>
        <artifactId>txlcn-txmsg-netty</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    
    
  • 表结构

    CREATE TABLE `t_tx_exception`  (
    `id` bigint(20) NOT NULL AUTO_INCREMENT,
    `group_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
    `unit_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
    `mod_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
    `transaction_state` tinyint(4) NULL DEFAULT NULL,
    `registrar` tinyint(4) NULL DEFAULT NULL COMMENT '-1:未知;0:Manager 通知事务失败;1:client询问事务状态失败;2:事务发起方关闭事务组失败'
    `ex_state` tinyint(4) NULL DEFAULT NULL COMMENT '0:待处理;1:已处理',
    `create_time` datetime(0) NULL DEFAULT NULL,
    PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 967 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
    
    CREATE TABLE `t_logger` (
      `id` int(10) NOT NULL AUTO_INCREMENT,
      `group_id` varchar(20) DEFAULT NULL,
      `unit_id` varchar(20) DEFAULT NULL,
      `tag` varchar(255) DEFAULT NULL,
      `content` varchar(255) DEFAULT NULL,
      `create_time` varchar(10) DEFAULT NULL,
      `app_name` varchar(50) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
    
  • TM属性配置

    spring.application.name=xxxxx
    server.port=7970
    
    # JDBC 数据库配置
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://ip:3306/tx-manager?characterEncoding=UTF-8
    spring.datasource.username=账号
    spring.datasource.password=秘密
    
    # redis 的设置信息. 线上请用Redis Cluster
    spring.redis.host=127.0.0.1
    spring.redis.port=6379
    spring.redis.password=xxxxx
    
    mybatis.configuration.map-underscore-to-camel-case=true
    mybatis.configuration.use-generated-keys=true
    
    # TM后台登陆密码
    tx-lcn.manager.admin-key=admin
    
    # TC连接IP, TM监听IP, 默认为 127.0.0.1
    tx-lcn.manager.host=127.0.0.1
    
    # TC连接端口,TM监听Socket端口
    tx-lcn.manager.port=8070
    
    # 心跳检测时间(ms)
    #tx-lcn.manager.heart-time=15000
    
    # 分布式事务执行总时间
    #tx-lcn.manager.dtx-time=30000
    
    #参数延迟删除时间单位ms
    #tx-lcn.message.netty.attr-delay-time=10000
    #tx-lcn.manager.concurrent-level=128
    
    # 开启日志,默认为false
    tx-lcn.logger.enabled=true
    tx-lcn.logger.driver-class-name=${spring.datasource.driver-class-name}
    tx-lcn.logger.jdbc-url=${spring.datasource.url}
    tx-lcn.logger.username=${spring.datasource.username}
    tx-lcn.logger.password=${spring.datasource.password}
    
    
  • TM启动类添加注解
    @EnableTransactionManagerServer

  • TC属性配置

    spring.application.name=spring-demo-d
    server.port=12002
    ## 数据库配置
    spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://127.0.0.1:3306/txlcn-demo?characterEncoding=UTF-8&serverTimezone=UTC
    spring.datasource.username=root
    spring.datasource.password=roo
    
    mybatis.type-aliases-package=xxx.xxx.xxx
    mybatis.mapper-locations=classpath:mapper/*.xml
    
    ## tx-manager 链接配置,集群逗号分隔
    tx-lcn.client.manager-address=127.0.0.1:8070
    
  • TC 启动类添加注解
    @EnableDistributedTransaction

  • 业务层添加注解
    @Transactional(rollbackFor = Exception.class)
    @LcnTransaction

负载与集群配置(TX-LCN)

  • 首选需要启动多个TxManager服务。
    server.port=xxxx
    
  • 在客户端配置TxManager服务地址。
    tx-lcn.client.manager-address=127.0.0.1:xxxx,127.0.0.1:xxxx
    
  • 目前TX-LCN的负载机制仅提供了随机机制。
    tx-lcn.springcloud.loadbalance.enabled=true
    

TCC( Try、Confirm、Cancel)

原理

TCC事务机制是通过对(由业务系统提供的)业务逻辑的调度来实现分布式事务。主要由三步操作,Try: 尝试执行业务、 Confirm:确认执行业务、 Cancel: 取消执行业务。

特点

  • 该模式对代码的嵌入性高,要求每个业务需要写三种步骤的操作。
  • 该模式对有无本地事务控制都可以支持,使用面广。
  • 数据一致性控制几乎完全由开发者控制,对业务开发难度要求高。

原理图

在这里插入图片描述

业务实现

  • 1、业务方法
    @Transactional(rollbackFor = Exception.class)
    @TccTransaction
  • 2、confirm方法
    public Object confirmXXXX(){}
  • 3、cancel方法(业务逆向处理)
    public Object cancelXXXX(){}
  • ThreadLocal tl=new ThreadLocal();处理线程数据

Seata

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

核心组件

  • Transaction Coordinator(TC):事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
  • Transaction Manager(TM):控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
  • Resource Manager(RM):控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。

原理图

在这里插入图片描述
一个典型的事务过程包括:

  • 1、TM 向 TC 申请开启(Begin)一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
  • 2、XID 在微服务调用链路的上下文中传播。
  • 3、RM 向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖。
  • 4、TM 向 TC 发起针对 XID 的全局提交(Commit)或回滚(Rollback)决议。
  • 5、TC 调度 XID 下管辖的全部分支事务完成提交(Commit)或回滚(Rollback)请求。

事务模式

AT模式写隔离、读隔离
在这里插入图片描述

AT 模式 RM 驱动分支事务的行为分为以下两个阶段:

  • 执行阶段:
    1、代理 JDBC 数据源,解析业务 SQL,生成更新前后的镜像数据,形成 UNDO LOG。
    2、向 TC 注册分支。
    3、分支注册成功后,把业务数据的更新和 UNDO LOG 放在同一个本地事务中提交。
  • 完成阶段:
    1、全局提交,收到 TC 的分支提交请求,异步删除相应分支的 UNDO LOG。
    2、全局回滚,收到 TC 的分支回滚请求,查询分支对应的 UNDO LOG 记录,生成补偿回滚的 SQL 语句,执行分支回滚并返回结果给 TC。

在这里插入图片描述
TCC 模式 RM 驱动分支事务的行为分为以下两个阶段:

  • 执行阶段:
    1、向 TC 注册分支。
    2、执行业务定义的 Try 方法。
    3、向 TC 上报 Try 方法执行情况:成功或失败。
  • 完成阶段:
    1、全局提交,收到 TC 的分支提交请求,执行业务定义的 Confirm 方法。
    2、全局回滚,收到 TC 的分支回滚请求,执行业务定义的 Cancel 方法。

TCC问题

  • 空回滚:try未执行,cancel执行,业务回滚,解决方案,加入记录,try的执行标记,如果try没有执行,cancel空回滚;
  • 幂等:cancel/confirm多次执行,解决方案,加入记录标记cancel/confirm的执行标记,如果有对应标记,不执行;
  • 悬挂:cancel在try之前执行,解决方案,canncel在空回滚时插入回滚记录,try继续执行时发现有回滚记录 ,空try;

在这里插入图片描述

Saga 模式 RM 驱动分支事务的行为包含以下两个阶段:

  • 执行阶段:
    1、向 TC 注册分支。
    2、执行业务方法。
    3、向 TC 上报业务方法执行情况:成功或失败。
  • 完成阶段:
    1、全局提交,RM 不需要处理。
    2、全局回滚,收到 TC 的分支回滚请求,执行业务定义的补偿回滚方法。

在这里插入图片描述
XA 模式 RM 驱动分支事务的行为包含以下两个阶段:

  • 执行阶段:
    1、向 TC 注册分支。
    2、XA Start,执行业务 SQL,XA End。
    3、XA prepare,并向 TC 上报 XA 分支的执行情况:成功或失败。
  • 完成阶段:
    1、收到 TC 的分支提交请求,XA Commit。
    2、收到 TC 的分支回滚请求,XA Rollback。

事务消息(rocketmq)

相关概念

  • 1、Half(Prepare) Message——半消息(预处理消息)
    半消息是一种特殊的消息类型,该状态的消息暂时不能被Consumer消费。当一条事务消息被成功投递到Broker上,但是Broker并没有接收到Producer发出的二次确认时,该事务消息就处于"暂时不可被消费"状态,该状态的事务消息被称为半消息。
  • 2、Message Status Check——消息状态回查
    由于网络抖动、Producer重启等原因,可能导致Producer向Broker发送的二次确认消息没有成功送达。如果Broker检测到某条事务消息长时间处于半消息状态,则会主动向Producer端发起回查操作,查询该事务消息在Producer端的事务状态(Commit 或 Rollback)。可以看出,Message Status Check主要用来解决分布式事务中的超时问题。
  • 3、消息状态
    • 1、TransactionStatus.CommitTransaction,提交事务,表示允许消费者消费(使用)这条消息

    • 2、TransactionStatus.RollbackTransaction,回滚事务,表示消息将被删除,不允许使用

    • 3、TransactionStatus.Unknown,中间状态,表示需要MQ向消息发送方进行检查以确定状态

原理图

在这里插入图片描述

执行流程

  • 1、Producer向Broker端发送Half Message;
  • 2、Broker ACK,Half Message发送成功;
  • 3、Producer执行本地事务(executeLocalTransaction);
  • 4、本地事务完毕,根据事务的状态,Producer向Broker发送二次确认消息,确认该Half Message的Commit或者Rollback状态。Broker收到二次确认消息后,对于Commit状态,则直接发送到Consumer端执行消费逻辑,而对于Rollback则直接标记为失败,一段时间后清除,并不会发给Consumer。正常情况下,到此分布式事务已经完成,剩下要处理的就是超时问题,即一段时间后Broker仍没有收到Producer的二次确认消息;
  • 5、针对超时状态,Broker主动向Producer发起消息回查(checkLocalTransaction);
  • 6、Producer处理回查消息,返回对应的本地事务的执行结果;
  • 7、Broker针对回查消息的结果,执行Commit或Rollback操作,同Step4。
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值