分布式事务专题

事务的概念

参考分布式事务概论
严格意义上的事务实现应该是具备原子性、一致性、隔离性和持久性,简称 ACID。
原子性(Atomicity),可以理解为一个事务内的所有操作要么都执行,要么都不执行。
一致性(Consistency),可以理解为数据是满足完整性约束的,也就是不会存在中间状态的数据
隔离性(Isolation),指的是多个事务并发执行的时候不会互相干扰
持久性(Durability),指的是一个事务完成了之后数据就被永远保存下来

分布式事务:

分布式事务是在一个JVM实例中调用不同的服务形成事务,不同的服务可能不在同一个JVM实例中,可能涉及到操作多个数据库的事务,必须保证每个独立节点上的数据操作可以满足ACID。

跨服务间的通讯(不同的数据库连接)都会产生分布式事物问题。

具体应用场景,包括以下三个:A、服务内跨数据库的事务;B、跨内部服务的事务;C、跨外部服务的事务。

其中划分内部和外部的标准是:内部服务我们可以控制其实现,修改配置或代码;外部服务指的是第三方的,只能约定通信的方式和具体协议,具体代码实现在控制范围之外。

,解决分布式事务理论基础

分布式事务中的“事务”要区分与数据库事务,实现分布式事务的理论主要为CAP理论及BASE理论。

CAP定理是由加州大学伯克利分校Eric Brewer教授提出来的,他指出WEB服务无法同时满足一下3个属性:

一致性(Consistency) : 客户端知道一系列的操作都会同时发生(生效)

可用性(Availability) : 每个操作都必须以可预期的响应结束

分区容错性(Partition tolerance) : 即使出现单个组件无法可用,操作依然可以完成

BASE理论

在分布式系统中,我们往往追求的是可用性,它的重要性比一致性要高,那么如何实现高可用性呢? 前人已经给我们提出来了另外一个理论,就是BASE理论,它是用来对CAP定理进行进一步扩充的。BASE理论指的是:

Basically Available(基本可用)

Soft state(软状态)

Eventually consistent(最终一致性)

BASE理论是对CAP中的一致性和可用性进行一个权衡的结果,理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。

分布式事务解决方案

1,强一致性事务演变:2PC 和 3PC, TCC

2PC 和 3PC 是一种强一致性事务,不过还是有数据不一致,阻塞等风险,而且只能用在数据库层面。

而 TCC 是一种补偿性事务思想,适用的范围更广,在业务层面实现,因此对业务的侵入性较大,每一个操作都需要实现对应的三个方法。

2,由于中间件等服务广泛应用,对一些实时性要求不高的业务场景,实现最终一致性成为了越来越流行的分布式事务解决方案,

实现有:本地消息、事务消息和最大努力通知其实都是最终一致性事务,因此适用于一些对时间不敏感的业务。

两阶段提交(2PC)

2PC即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段( Prepare phase).提交阶段( commit phase )

在第一阶段(准备阶段),事务管理器TM(协调者)先向事务参与者(资源RM)们发送准备请求,大家都返回OK状态,那么就进入第二阶段,提交事务,如果在第一阶段有任何一个参与者没有OK,那么事务协调器通知其他所有事务参与者(资源RM)回滚事务。

二阶段能保证分布式事务的原子性,但是也有一些明显的缺陷:

在第一阶段,如果参与者迟迟不回复协调者,就会造成事务的阻塞,性能不好。

单节点故障,如果协调器挂了,参与者会阻塞,比如在第二阶段,如果事务协调器宕机,参与者没办法回复信息,长时间处于事务资源锁定,造成阻塞(事务操作是要加锁的)。

在第二阶段,如果在事务协调器发出"commit"执行后宕机,一部和参与者收到了消息提交了事务,而 一部分没有消息没法做出事务提交操作,这样就出现了数据不一致。

在第二阶段,如果事务事务协调器发出“commit”指令后宕机,收到“commmit”指令的参与者也宕机了, 那么事务最终变成了什么效果,提交了还是没提交?没有谁知道。

总之RM在执行分支事务后需要等待TM的决定,此时服务会阻塞并锁定资源。由于其阻塞机制和最差时间复杂度高,因此,这种设计不能适应随着事务涉及的服务数量增加而扩展的需要,很难用于并发较高以及子事务生命周期较长(long-running transactions)的分布式服务中。

3PC:

三阶段提交协议主要是为了解决两阶段提交协议的阻塞问题,2PC存在的问题是当协作者崩溃时,参与者不能做出最后的选择。因此参与者可能在协作者恢复之前保持阻塞。三阶段提交(Three-phase commit),是二阶段提交(2PC)的改进版本。

与两阶段提交不同的是,三阶段提交有两个改动点。

引入超时机制。同时在协调者和参与者中都引入超时机制。
在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

CanCommit: 这里是资源准备阶段(这个时候不会上锁)
PreCommit: 这里是资源确认阶段(上锁)
DoCommit: 这跟2PC的二阶段差不多

TCC补偿机制

TCC 其实就是采用补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段:

Try 阶段主要是对业务系统做检测及资源预留

Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。

Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。

TCC实际上把数据库层的二阶段提交上提到了应用层来实现对于数据库来说是一阶段提交,避免了数据库层的2PC性能低下的问题,而且上升到业务层之后再事务处理中可以不仅仅是针对数据库。但是TCC操作需要业务实现,开发成本较高。

这种分布式事务的实现方式的优势在于,可以让应用自己定义数据操作的粒度,使得降低锁冲突、提高吞吐量成为可能。而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、 cancel三个操作。 此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。

MQ异步确保型策略

MQ异步确保型是指通过消息的一致性来处理事务。当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。

最终一致性要解决三个问题:本地事务和消息发送的原子性,接收消息的可靠性,消息重复消费问题

本地事务和消息发送原子性

本地事务与消息发送的原子性问题即:事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最终一致性方案的关键问题。

事务参与方接收消息可靠性 ack机制

务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息

消息重复消息问题 幂等性校验

由于网络2的存在,若某一个消费节点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息的重复消费。要解决消息重复消费的问题就要实现事务参与方的方法幂等性。

可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作,避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。典型的使用场景:注册送积分,登录送优惠券等。

最大努力通知型

最大努力通知服务表示在不影响主业务的情况下,尽可能地确保数据的一致性。它需要开发人员根据业务来指定通知规则,在满足通知规则的前提下,尽可能的确保数据的一致,以达到最大努力的目的。

目标:发起通知方(接口提供方)通过一定的机制最大努力将业务处理结果通知到接收方(接口调用方)。

具体包括:

有一定的消息重复通知机制。

因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。 10s 1min 10min 1h 5h 1d

消息校对机制。

如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知 方查询消息信息来满足需求。

消息处理方需要保证幂等性

最大努力通知与MQ异步确保有什么不同?

可靠消息最终一致性

系统A本地事务执行成功,通知系统B处理任务,通常通过MQ实现。一般适用于平台内部,对一 致性要求相对较高(微服务的2个子系统之间)。

最大努力通知

所谓最大努力通知就是系统A用最大努力通知系统B,能不能成功,不做完全保证,如果没通知到位,系统B可以主动来调用系统A的接口查询结果状态。一般适用于跨平台业务,或对接了上方平台的业 务场景(支付结果通知)。

最大努力通知是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务 ;允许发起通知方处理业务失败,在接收通知方收到通知后积极进行失败处理,无论发起通知方如何处理结果都会不影响到接收通知方的后续处理;发起通知方需提供查询执行情况接口,用于接收通知方校对结果。典型的使用 场景:银行通知、支付结果通知等。

Seata基础

Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。

Seata的工作原理

Seata主要用的是基于二阶段提交思想的AT模式,通过注解实现非业务侵入。
TC(Transaction Coordinator),事务协调者,在源码中Seata Server充当事务协调者身份,维护全局锁状态,协调全局事务的提交与回滚。

TM(Transaction Manager),事务管理者,业务代码中使用了全局事务注解的服务属于事务管理者,控制全局事务的范围,执行全局事务的提交与回滚。

RM(Resource Manager),资源管理者,业务代码中被远程调用的部分,负责执行本地事务,和提交与回滚本地事务。

第一阶段所有的RM执行自己的本地事务。在执行本地事务时,用大白话讲其实就是jdbc执行sql时,seata使用了数据源代理,在执行sql前,对sql进行解析,生成前置镜像sql,后置镜像sql,同时向undo log插入一条数据,方便后期万一出现异常做回滚,然后向TC注册分支事务,提交本地事务,最后向TC提交它的分支事务状态。

二阶段流程分两种情况

所有RM本地事务执行成功,此时TM会向TC发起全局事务提交,TC会立马释放全局锁然后异步驱动所有RM做分支事务的提交。

存在一个RM本地事务不成功,此时TM会向TC发起全局事务回滚,TC会驱动所有的RM做回滚操作,等待所有的RM回滚成功后然后再释放全局锁。

这个阶段所有RM提交分支事务,其实就是删除Undo Log表里的记录,如果提交分支事务失败,并不会影响业务数据,可以手动的做Undo Log删除。

这个阶段,所有的RM执行分支事务回滚,此时是去Undo Log表中查找数据,然后通过第一阶段生成的后置镜像sql,与数据进行校验,通过前置镜像sql做回滚,然后删除Undo Log日志。

Seata的下载

打开官方网站的下载页:http://seata.io/zh-cn/blog/download.html

Seata的配置

这里我们需要对两个配置文件进行设置,首先打开registry.conf:

#这里主要是配置服务的注册中心,Seata的服务器也是一个微服务
registry {
  # 注册中心可以是多种形式: file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  # type的值就是需要选择的形式,我们选择用当前项目使用的nacos  
  type = "nacos"
  loadBalance = "RandomLoadBalance"
  loadBalanceVirtualNodes = 10

  #选择nacos后相关的配置  
  nacos {
    application = "seata-server"
    serverAddr = "localhost:8848"
    group = "SEATA_GROUP"
    namespace = "public"
    cluster = "DEFAULT"
    username = "nacos"
    password = "nacos"
  }
  
  #后面是选择各个不同的注册中心需要的配置,我们不需要去做改动
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = 0
    password = ""
    cluster = "default"
    timeout = 0
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

#这里是服务端的相关参数配置
config {
  #参数配置也可以选择不同的形式: file、nacos 、apollo、zk、consul、etcd3
  #type的值就是选择的具体配置形式,我们选择的是文件配置  
  type = "file"

  #这些是其他配置方式的各个配置,可以不用改动
  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = ""
    password = ""
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    appId = "seata-server"
    apolloMeta = "http://192.168.1.204:8801"
    namespace = "application"
    apolloAccesskeySecret = ""
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }

  #文件配置指出文件是谁
  file {
    name = "file.conf"
  }
}

参数配置文件(file.conf)

#参数配置里面最主要的是数据源的配置,因为Seata在处理事务的过程中会存储一些临时数据,所以需要数据源
store {
  # 配置数据源的类型,可以是 db(数据库)、file、redis
  # 这里我们选择用数据库  
  mode = "db"

  ## 这是用文件存储的配置 不用改动
  file {
    dir = "sessionStore"
    maxBranchSessionSize = 16384
    maxGlobalSessionSize = 512
    fileWriteBufferCacheSize = 16384
    sessionReloadReadSize = 100
    flushDiskMode = async
  }

  ## 这是配置选择数据库时候的连接配置
  db {
    ## 连接池配置,可以是druid、dbcp、hikari
    datasource = "druid"
    ## 数据库的类型,我们选择mysql
    dbType = "mysql"
    ## 下面是数据库的常规配置
    driverClassName = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata"
    user = "root"
    password = "123321"
    minConn = 5
    maxConn = 100
    ## 下面这三个配置是数据库需要用到的表示谁?数据库存储会涉及到表,这里有三张表
    ## 这三张表是Seata提供的,我们只需要导入DDL就可以
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
    maxWait = 5000
  }

  ## 这是选择用redis作为数据源的配置,不用改动
  redis {
    host = "127.0.0.1"
    port = "6379"
    password = ""
    database = "0"
    minConn = 1
    maxConn = 10
    maxTotal = 100
    queryLimit = 100
  }

}

数据库建表

在前面file的配置里面数据库需要三张表,这三张表在1.4.0这个版本下面它没有提供,所以我们去0.9.0的conf目录下面:

在上面的目录结构里面还有一个sql文件db_undo_log.sql,这个也是在执行事务的过程中需要用到的表,但并不是Seata服务器直接操作的表,这是我们自己的业务服务里面需要操作的表,所以在项目自己的每个数据库里面也需要插入这个表

启动服务

Seata的AT模式
AT模式的原理
Seata在实现分布式事务上有几种不同的方式,最主要的就是AT模式与TCC模式,我们首先来看一下AT模式的实现原理。

Seata的AT模式建立在关系型数据库的本地事务特性的基础之上,通过数据源代理类拦截并解析数据库执行的SQL,记录自定义的回滚日志,如需回滚,则重放这些自定义的回滚日志即可。AT模式虽然是根据XA事务模型(2PC)演进而来的,但是AT打破了XA协议的阻塞性制约,在一致性、和性能上取得了平衡。

在Seata in AT mode下,上体描述的全局事务执行流程为:

service2向Seata注册全局事务,并产生一个全局事务标识XID

service1.DB1、service2.DB2、service3.DB3向Seata注册分支事务,并将其纳入该XID对应的全局事务范围

service1.DB1、service2.DB2、service3.DB3向Seata汇报本地事务的准备状态

Seata汇总所有的DB的本地事务的准备状态,决定全局事务是该提交还是回滚

Seata通知service1.DB1、service2.DB2、service3.DB3提交/回滚本地事务

AT模式是基于XA事务模型演进而来的,可以看出它的整体机制也是一个改进版本的两阶段提交协议。AT模式的两个基本阶段是:

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

第二阶段:提交异步化,这个过程很快就能完成。若需要回滚,则通过第一阶段的回滚日志进行反向补偿。

AT模式的两个阶段到底干了什么,我们来看一下:

第一阶段:

开启本地事务

对响应的业务表进行写操作

将写操作的记录快照组成一个回滚日志插入undo_log表(第二阶段如果Seata需要回滚回访该快照即可)

事务提交之前向seata注册分支事务,申请全局锁,避免其他事务干扰

提交本地事务,包括业务数据的修改、undo_log表的插入

向Seata server报告本地事务的提交结果

第二阶段:

第一阶段结束之后,Seata会接收到所有分支事务的提交状态,然后决定是提交全局事务还是回滚全局事务。

若所有分支事务本地提交均成功,则Seata决定全局提交。Seata将分支提交的消息发送给各个分支事务,各个分支事务收到分支提交消息后,会将消息放入一个缓冲队列,然后直接向Seata返回提交成功。之后,每个本地事务会慢慢处理分支提交消息,处理的方式为:删除相应分支事务的undo_log记录。之所以只需删除分支事务的undo_log记录,而不需要再做其他提交操作,是因为提交操作已经在第一阶段完成了(这也是AT和XA不同的地方)。
若任一分支事务本地提交失败,则Seata决定全局回滚,将分支事务回滚消息发送给各个分支事务,由于在第一阶段各个服务的数据库上记录了undo_log记录,分支事务回滚操作只需根据undo_log记录进行补偿即可。
AT模式应用
AT模式的实现其实非常简单,我们只需要一个全局事务的注解(@GlobalTransactional(rollbackFor = Exception.class))添加到业务方法上即可。

@GlobalTransactional(rollbackFor = Exception.class)
@Override
public void createOrder() {
	Order order = new Order();
	order.setNum("WONIU202103100067");
	save(order);
	int i = 1/0;
	productClient.updateCont();
}

TCC 模式

不依赖于底层数据资源的事务支持:

一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
二阶段 commit 行为:调用 自定义 的 commit 逻辑。
二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。

二、使用流程:

1、注解和配置

TCC 模式的 TM 跟 AT 模式的使用类似,只要在方法上面加上 @GlobalTransactional 注解就就可以了, file.conf 和 registry.conf 配置都跟 AT 模式一样。

2、TCC 的 RM 的使用配置:

RM 的接口上面必须要有@LocalTCC 注解,且必须在接口上面,如图核心接口代码:

@LocalTCC//核心注解

public interface IKillService {
    //核心方法配置
    @TwoPhaseBusinessAction(name = "updateByIdTcc", commitMethod = "updateIdTccCommit",
            rollbackMethod = "updateTccRollback")
    @RequestMapping(value = "/updateByIdTcc",
            method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_VALUE)
    boolean updateByIdTcc(@RequestBody BusinessActionContext businessActionContext,
                          @BusinessActionContextParameter(paramName = "killGoodsPriceStr")
                          @RequestParam("killGoodsPriceStr") String killGoodsPriceStr);
 
    //提交方法
    @RequestMapping(value = "/updateIdTccCommit", method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_VALUE)
    boolean updateIdTccCommit(@RequestBody BusinessActionContext businessActionContext);
 
    //回滚方法
    @RequestMapping(value = "/updateTccRollback", method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_VALUE)
    boolean updateTccRollback(@RequestBody BusinessActionContext businessActionContext);
 
}

TCC 使用过程中的几个典型问题及解决方案

1、空回滚

  首先是空回滚。什么是空回滚?空回滚就是对于一个分布式事务,在没有调用 TCC 资源 Try 方法的情况下,调用了二阶段的 Cancel 方法, Cancel 方法需要识别出这是一个空回滚,然 后直接返回成功。 什么样的情形会造成空回滚呢?如果调用 try 逻辑有问题,比如调用方机器宕机、网络异常, 都会造成 RPC 调用失败,即未执行 Try 方法。但是分布式事务已经开启了,需要推进到 终态,因此,TC 会回调参与者二阶段 Cancel 接口,从而形成空回滚。

解决方案:

    需要一张额外的事务控制表,其中有分布式事务 ID 和分支事务 ID ,第一阶段 Try 方法里 会插入一条记录,表示一阶段执行了。Cancel 接口里读取该记录,如果该记录存在,则正

常回滚;如果该记录不存在,则是空回滚。

2、悬挂

  悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。因为允许空回滚 的原因,Cancel 接口认为 Try 接口没执行,空回滚直接返回成功,对于 Seata 框架来说,

认为分布式事务的二阶段接口已经执行成功,整个分布式事务就结束了。 但是这之后 Try 方 法才真正开始执行,预留业务资源,前面提到事务并发控制的业务加锁,对于一个 Try 方
法预留的业务资源,只有该分布式事务才能使用,然而 Seata 框架认为该分布式事务已经 结束,也就是说,当出现这种情况时,该分布式事务第一阶段预留的业务资源就再也没有人
能够处理了,对于这种情况,我们就称为悬挂,即业务资源预留后没法继续处理。

解决方案:

可以在二阶段执行时插入一条事务控制记录,状态为已回滚,这样当一阶段执行时,先读取 该记录,如果记录存在,就认为二阶段已经执行;否则二阶段没执行。

3、幂等

   幂等就是对于同一个分布式事务的同一个分支事务,重复去调用该分支事务的第二阶段接 口,因此,要求 TCC 的二阶段 Confirm 和 Cancel 接口保证幂等,不会重复使用或者释

放资源。如果幂等控制没有做好,很有可能导致资损等严重问题。 什么样的情形会造成重复提交或回滚?从图中可以看到,提交或回滚是一次 TC 到参与者 的网络调用。因此,网络故障、参与者宕机等都有可能造成参与者 TCC 资源实际执行了二 阶段防范,但是 TC 没有收到返回结果的情况,这时, TC 就会重复调用,直至调用成功,
整个分布式事务结束。

解决方案:

    一个简单的思路就是记录每个分支事务的执行状态。在执行前状态,如果已执行,那就不再 执行;否则,正常执行。前面在讲空回滚的时候,已经有一张事务控制表了,事务控制表的 每条记录关联一个分支事务,那我们完全可以在这张事务控制表上加一个状态字段,用来记 录每个分支事务的执行状态。除此之外我们还应该要有业务幂等,比如查询支付状态,如果 是已经支付就不让再次支付了,直接结束二阶段。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值