dubbo整体设计整理

dubbo整体组件设计、调用过程

API层

Service

interface 业务层。包括业务代码的接口与实现,即开发者实现的业务代码

config

ConfigParser配置层。主要围绕ServiceConfig (暴露的服务配置)和 ReferenceConfig (引用的服务配
置)两个实现类展开,初始化配置信息。可以理解为该层管理了整个Dubbo的配置

ServiceConfig:doExport服务暴露的主要实现,dubbo首先应该是扫描了@DubboService相关注解、配置,然后在暴露服务的流程里通过doExport,基础校验加载配置信息构建service的信息,然后通过doExportUrls()向注册中心注册支持的协议,构建服务URL的协议信信息,比如token、协议版本号、进程等,然后通过动态编译生成invoker,registryURL存储的是注册中心地址,使用export作为key追加服务元数据信息,protocol.export暴露服务信息,默认是通过dubbo协议,构建服务key存储到本地map中,便于后续提供服务时,找到目标服务,然后通过openServer将服务暴露出去,与注册中心建立连接、产生心跳任务,这样一个服务就完成了暴露.

ReferenceConfig:init方法会将远程调用服务进行初始化,加载消费者配置信息、进行基础校验、配置填充,接着调用createProxy()方法,若是本地有该接口的实现refer,则直接调用本地方法,不进行RPC调用,否则调用refprotocol.refer(),通过RegistryProtocol的refer方法执行注册中心实例的创建,元数据注册到注册中心及订阅的功能,通过 Cluster 合并 directory的方式 cluster.join(directory)来组成Invoker,这样到执行相关的远程调用时,相关的集群容错就可以通过directory的数据来及时的得到最新的服务信息,然后再通过相关的路由机制、集群容错机制、负载均衡机制,来发起调用.路由机制是在directory中获取list时筛选的,应该是从注册中心上拉取的,提供者的一些标识,比如多机房等,消费者需要判定自己能否发起调用.
Dobbo协议会发送readonly事件报文通知consumer服务不可用

SPI层

proxy

ProxyFactory服务代理层。在Dubbo中,无论生产者还是消费者,框架都会生成一个代理类,整个过
程对上层是透明的。当调用一个远程接口时,看起来就像是调用了一个本地的接口一样,
代理层会自动做远程调用并返回结果,即让业务层对远程调用完全无感

整个远程调用 的过程对开发者完全是透明的,就像本地调用一样。这正是由于ProxyFactory帮我们生成了代理类,当我们调用某个远程接口时,实际上使用的是代理类.Dubbo中的 ProxyFactory有两种默认实现:Javassist和JDK,用户可以自行扩展自己的实现,如CGLIB(Code Generation Library)o Dubbo选用Javassist作为默认字节码生成工具,主要是基于性能和使用的 简易性考虑,Javassist的字节码生成效率相对于其他库更快,使用也更简单
代理类远程调用过程:服务接口调用proxy代理类调用到cluster中的Invoker,然后得到目标服务器,根据protocol通过责任链模式,依次执行消费者端的filter,然后执行invoker,发起远程调用(动态代理,需要运用到动态编译的技术。虽然我们也可以用反射的技术实现,但是相比来说,还是有一定的性能差距)
Javassist 的主要的优点,在于简单,而且快速,直接使用 Java 编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类

Registry

Registry、RegistryFactory、RegistryService
注册层。负责Dubbo框架的服务注册与发现。当有新的服务加入或旧服务下线时,注册 中心都会感知并通知给所有订阅方。整个过程不需要人工参与

一、ZooKeeper注册中心采用的是“事件通知” + “客户端拉取”的方式,客户端在第一次连接上注册中心时,会获取对应目录下全量的数据。并在订阅的节点上注册一个watcher,客户端与注册中心之间保持TCP长连接,后续每个节点有任何数据变化的时候,注册中心会根据watcher的回调主动通知客户端(事件通知),客户端接到通知后,会把对应节点下的全量数据都拉取过 来(客户端拉取)

二、FailbackRegistry抽象类中定义了一个ScheduledExecutorService, 每经过固定间隔(默认 为5秒)调用FailbackRegistry#retry()方法.在定时器中调用retry方法的时候,会把这五个集合(发起注册失败、取消注册失败、发起订阅失败、取消订阅失败、通知失败)分别遍历和重试,重试成功则从集合中移除,这样在Registry模块就很完整了,主流程不需要关注重试,异常情况添加到失败数据中,让其自行调度就可以了

三、RegistryService定义了接口方方法,通过zookerperRegistry等不同注册中心完成具体实现,拉取的数据在RegistryDirectory中存储,RegistryProtocol,应该是Registry包中提供的,便于协议到注册中心进行暴露接口、获取目标的Invoker的作用

Cluster

Cluster、Directory、LoadBalance 集群容错层。该层主要负责:远程调用失败时的容错策略(如失败重试、快速失败);
选择具体调用节点时的负载均衡策略(如随机、一致性Hash等);特殊调用路径的路由 策略(如某个消费者只会调用某个IP的生产者)

Cluster看作一个集群容错层,该层中包含Cluster、Directory、Router、LoadBalance 几大核心接口
Abstractclusterinvoker的invoke()是主要的逻辑,获取当前线程的隐式参数加入到invocation中,通过Directory、Router得到可以调用的服务列表,通过URL中的负载均衡参数(默认是随机数+权重)来使用相应的负载策略,通过具体集群容错机制的doInvoke方法来发起调用,通过父类的select()->doSelect()使用负载策略得到目标服务器,然后发起调用

集群容错(默认是Failover,个人感觉Failover、Failfast、Failsafe、Fallback、Mock会相对常用一些)
Failover:当出现失败时,会重试其他服务器。用户可以通过retries="2n设置重试次数。这是Dubbo的默认容错机制,会对请求做负载均衡。通常使用在读操作或幕等的写操作上,但重试会导致接口的延迟增大,在下游机器负载已经达到极限时,重试容易加重下游服务的负载
Failfast:快速失败,当请求失败后,快速返回异常结果,不做任何重试。该容错机制会对请求做负载均衡,通常使用在非幕等接口的调用上。该机制受网络抖动的影响较大
Failsafe:当出现异常时,直接忽略异常。会对请求做负载均衡。通常使用在“佛系”调用场景,即不关心调用是否成功,并且不想抛异常影响外层调用,如某些不重要的日志同步,即使出现异常也无所谓
Fallback:请求失败后,会自动记录在失败队列中,并由一个定时线程池定时重试,适用于一些 异步或最终一致性的请求。请求会做负载均衡
Forking:同时调用多个相同的服务,只要其中一个返回,则立即返回结果。用户可以配置forks:最大并行调用数”参数来确定最大并行调用的服务数量。通常使用在对接口 实时性要求极高的调用上,但也会浪费更多的资源
**Broadcast:**广播调用所有可用的服务,任意一个节点报错则报错。由于是广播,因此请求不需要 做负载均衡。通常用于服务状态更新后的广播
Mock:提供调用失败时,返回伪造的响应结果。或直接强制返回伪造的结果,不会发起远程调用
Available:最简单的方式,请求不会做负载均衡,遍历所有服务列表,找到第一个可用的节点, 直接请求并返回结果。如果没有可用的节点,则直接抛出异常
Mergeable:Mergeable可以自动把多个节点请求得到的结果进行合并

AbstractDirectory:list()方法根据方法名、参数信息获取服务列表,路由策略生效时,会通过router过滤掉不符合条件的服务提供者
RegistryDirectory:第一条,框架与注册中心的订阅,并动态更新本地Invoker列表、路由列表、配置信息的逻辑;第二条,子类实现父类的doList方法,因为RegistryDirectory是记录了从注册中心中拉取的数据,所以这里很方便去根据URL中的信息获取可调用的服务列表

router:路由规则,像是多机房、流量隔离等,通过tag就可以解决.

Abstractclusterinvoker:select()
(1) 检查URL中是否有配置粘滞连接,如果有则使用粘滞连接的Invoker。如果没有配置粘滞连接,或者重复调用检测不通过、可用检测不通过,则进入第2 步。
(2) 通过ExtensionLoader获取负载均衡的具体实现,并通过负载均衡做节点的选择。对选择出来的节点做重复调用、可用性检测,通过则直接返回,否则进入第3 步。
(3) 进行节点的重新选择。如果需要做可用性检测,则会遍历Directory中得到的所有节点,过滤不可用和已经调用过的节点,在剩余的节点中重新做负载均衡;如果不需要做可用性 检测,那么也会遍历Directory中得到的所有节点,但只过滤已经调用过的,在剩余的节点中重新做负载均衡。这里存在一种情况,就是在过滤不可用或已经调用过的节点时,节点全部被过滤,没有剩下任何节点,此时进入第4 步。
(4) 遍历所有已经调用过的节点,选出所有可用的节点,再通过负载均衡选出一个节点并 返回。如果还找不到可调用的节点,则返回null。

负载均衡策略:
RandomLoadBalance:随机,按权重设置随机概率。在一个节点上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者的权重
RoundRobinLoadBalance:轮询,按公约后的权重设置轮询比例。存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没“挂”,当请求调到第二台时就卡在那里,久而久之,所有请求都卡在调到第二台上
LeastActiveLoadBalance:最少活跃调用数,如果活跃数相同则随机调用,活跃数指调用前后计数差。使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大
ConsistentHashLoadBalance:一致性Hash,相同参数的请求总是发到同一提供者。当某一台提供者“挂”时,原本发往该提供者的请求,基于虚拟节点,会平摊到其他提 供者,不会引起剧烈变动。默认只对第一个参数“ Hash”,如果要修改, 则配置 <dubbo:parameter key=“hash.arguments” value=“0,1” />o默认使用160份虚拟节点,如果要修改,则配置〈dubbo:parameter key=“hash.nodes” value=“320” />
随机和轮训基于权重的,提供了预热功能,慢慢将请求分摊到新上线的机器,很不错哦,随机就是根据权重值,根据总权重值得到随机数,从而决定那个机器执行所以权重越大概率越高,轮训时会计算总权重,每台机器都会增加本机器的权重值,最大的那个权重值会作为目标服务,该服务的权重值,会减去总权重值,通过这个简易算法来保证轮训时基于权重.

Monitor

Monitor、MonitorFactory、MonitorService 监控层。这一层主要负责监控统计调用次数和调用时间等
这里没什么可看的逻辑,监控数据的上报是Filter里做的,监控平台展示没太多兴趣

Protocol

Protocol、Invoker、Exporter
远程调用层。封装RPC调用具体过程,Protocol是Invoker暴露(发布一个服务让别人
可以调用)和引用(引用一个远程服务到本地)的主功能入口,它负责管理Invoker的
整个生命周期。Invoker是Dubbo的核心模型,框架中所有其他模型都向它靠拢,或者
转换成它,它代表一个可执行体。允许向它发起invoke调用,它可能是执行一个本地的 接口实现,也可能是一个远程的实现,还可能一个集群实现

Protocol层扩展点主要是包含Protocol、Filter、ExportListener、InvokerListener.
Protocol是Dubbo RPC的核心调用层,具体的RPC协议都可以由Protocol点扩展。如果想增加一种新的RPC协议,则只需要扩展一个新的Protocol扩展点实现即可
Filter是Dubbo的过滤器扩展点,可以自定义过滤器,在Invoker调用前后执行自定义的逻 辑。在Filter的实现中,必须要调用传入的Invoker的invoke方法,否则整个链路就断了
ExporterListener和InvokerListener这两个扩展点非常相似,ExporterListener是在暴露和取消 暴露服务时提供回调;InvokerListener则是在服务的引用与销毁引用时提供回调。
protocol层主要是消费者filter执行,以及发送RPC的具体协议来组成,应该还包含相关的监听器实现,便于扩展(上面的两个listener).

过滤器:和web应用展的filter概念、作用是一样的,发起调用前的逻辑处理,过滤器链初始化,就是通过dubbo的SPI机制,在ProtocolFilterWrapper包装类实现了过滤器链的组装

MonitorFilter:监控并统计所有的接口的调用情况,如成功、失败、耗时。后续DubboMonitor会定时把该过滤器收集的数据发送到Dubbo-Monitor服务上(提供者、消费者都起作用,DubboMonitor中每分钟将统计信息发送到监控平台)
提供者端:
AccessLogFilter:打印每一次请求的访问日志。如果需要访问的日志只出现在指定的appender中,则可以在log的配置文件中配置additivity(每五秒执行一次日志记录)
ExecuteLimitFilter:用于限制服务端的最大并行调用数(通过信号量来控制并发度,executes配置可以设置控制提供者该接口并发调用限制,达到后,后续请求直接快速失败)
ClassLoaderFilter:用于切换不同线程的类加载器,服务调用完成后会还原回去
ContextFilter:为提供者者把一些上下文信息设置到当前线程 的 RpcContext 对象中,包括 invocation、localhost、remote host、隐式参数 等
EchoFilter:用于回声测试(消费者检测服务提供者是否可用,类似于ping,提供者端接收后,将第一个参数值返回,默认约定方法名$echo+一个参数为回声测试类型)

ExceptionFilter:用于统一的异常处理,防止出现序列化失败
GenericFilter:用于服务提供者端,实现泛化调用,实现序列化的检查和处理(方法名$invoke,且是三个参数(方法名、参数类型、参数值)的则认为是泛化调用)
TimeoutFilter:如果某些服务调用超时,则自动记录告警日志(发起调用达到超时时间,则记录warn日志,触发告警提醒)
TokenFilter:服务提供者下发令牌给消费者,通常用于防止消费者绕过注册中心直接调用服务提供者(提供者注册到注册中心会有token,若消费者没有从注册中心拉取,那么token校验就会拦截,也是安全考虑)
TpsLimitFilter:用于服务端的限流,注意与ExecuteLimitFilter区分(DefaultTPSLimiter是使用的令牌桶算法,来做的基于QPS的限流,ExecuteLimitFilter是并发调用数的限流)
TraceFilter:Trace指令的使用,和全局链路检测有关
消费者端:
ActiveLimitFilter
:用于限制消费者端对服务端的最大并行调用数(2.6有问题,如果消费者端大量请求过来打到阻塞这里,那么当正常请求释放链接后,从阻塞的唤醒一个,新请求在并发场景也啃一通过拦截,这样的话,真实放行的请求量会高于设置值,本质上还是校验和增加释放不是一个原子性的问题.3.0.7的优化,通过CAS的方式来保证原子性,也就是判断就通过while、CAS来处理,失败的,就阻塞等待最大到超时时间失败,当响应回来去扣减数值,然后notifyAll唤醒阻塞线程,去抢占接口执行权.改为notifyAll()感觉是避免notify()只对一个线程唤醒,万一该线程逻辑中已经到了时间,然后失败了,那么当前这个释放的执行权,阻塞的线程就无法拿到执行权了)
ConsumerContextFilter:为消费者把一些上下文信息设置到当前线程 的 RpcContext 对象中,包括 invocation、localhost、remote host、隐式参数 等
DeprecatedFilter:如果调用的方法被标记为已弃用,那么 DeprecatedFilter将记录一个错误消息
GenericImplFilter:用于服务消费者端,实现泛化调用,实现序列化的检查和处理
FutureFilter:在发起invoke或得到返回值、出现异常的时候触发回调事件(同步、异步时,针对响应、异常等情况的处理)

Exchange

Exchanger、ExchangeChannel、ExchangeClient、ExchangerServer
信息交换层。建立Request-Response模型,封装请求响应模式,如把同步请求转化为异 步请求

HeaderExchangeHandler应该是核心的请求、响应处理类,实现了ChannelHandler接口,ChannelHandler定义了在dubbo侧的通用接口,netty、mina进行适配了dubbo的这个接口,而HeaderExchangeHandler就是该接口核心实现,received()统一处理请求和响应,当时请求消息时,是常规请求时,通过handler.reply()调用DubboProtocol中的requestHandler的reply方法来执行filter链路、提供者的目标方法(ProtocolFilterWrapper),当是响应时,从消费者端的FUTURES的map中移除该请求ID的future,进行结果回填,从而唤醒阻塞的线程,从而消费者端可以正常执行业务.若callback不为空,在调度执行callback中的方法.DubboInvoker中doInvoke应该就是发起请求的真实逻辑,根据请求的单向、异步或者是同步,来决定如何处理返回的ResponseFuture对象,若是异步,则将结果填入RpcContext中的future中,不阻塞当前线程,若是同步,则阻塞等待直到超时,超时是RemotingInvocationTimeoutScan,遍历扫描超时数据触发结果回填,快速失败(3.0.7版本代码已经升级,使用时间轮来进行处理了)

Dubbo默认客户端和服务端都会发送心跳报文,用来保持TCP长连接状态。在客户端和服务端,Dubbo内部开启一个线程循环扫描并检测连接是否超时,在服务端如果发现超时则会主动关闭客户端连接,在客户端发现超时则会主动重新创建连接。默认心跳检测时间是60秒,详细可参见HeartBeatTask,和常规的心跳机制没什么区别

Transport

Codec、Transporter、ChannelHandler、Server/Client
网络传输层。把网络传输抽象为统一的接口,如Mina和Netty虽然接口不一样,但是
Dubbo在它们上面又封装了统一的接口。用户也可以根据其扩展接口添加更多的网络传输方式

Netty4的线程模型是:主从reactor线程模型,一个boss线程,min(cpu核数+1,32)个boss线程,boss线程连接事件,worker线程负责其他的事件(比如读、写事件,心跳消息)
handler处理相关业务时,netty线程转换为业务线程,默认以下业务线程模型策略
线程模型:
AllDispatcher:将所有I/O事件交给Dubbo线程池处理,Dubbo默认启用
ConnectionOrderedDispatcher:单独线程池处理连接、断开事件,和Dubbo线程池分开
DirectDispatcher:所有方法调用和事件处理在I/O线程中,不推荐
ExecutionDispatcher:只在线程池处理接收请求,其他事件在I/O线程池中
MessageOnlyDispatcher:只在线程池处理请求和响应时间,其他事件在I/O线程池中处理

业务线程池类型:
FixedThreadPool:默认的线程池类型,服务提供者端使用类型,200个线程数,队列大小为0,超过就会拒绝,拒绝策略是输出异常日志,jstack dump到文件中(10分钟一次),抛出拒绝执行的异常.使用时也可以根据暴露的url设置相应参数调大线程数值或者变为有界队列.
CachedThreadPool:dubbo 线程池客户端的默认实现是 cached,服务端的默认实现是 fixed,核心线程为0,最大为Integer最大值,队列默认为0,空闲活跃时间60秒(消费者端默认使用线程池类型,具体可参看 AbstractClient的wrapChannelHandler方法,通常是客户端接收回调执行业务的线程)
EagerThreadPool:当核心线程都处于繁忙状态时,创建新线程而不是将任务放入阻塞队列中 fixed,核心线程为0,最大为Integer最大值,队列默认为0,空闲活跃时间60秒,优先创建Worker线程池。在任务数量大于corePoolSize小于maximumPoolSize 时,优先创建Worker来处理任务。当任务数量大于maximumPoolSize时,将任务放入 阻塞队列。阻塞队列充满时抛出RejectedExecutionException (cached在任务数量超 过maximumPoolSize时直接抛出异常而不是将任务放入阻塞队列)。
LimitedThreadPool:可伸缩线程池,核心线程池0,最大线程池200,达到MAX.VALUE才会释放空闲线程,增大后基本很难降低,要空闲时间毫秒数要达到MAX_VALUE,目的是为了避免收缩时突然来了大流量引起的性能问题
3.0消费者线程优化:https://www.oomspot.com//post/chedubbo30gaojiyongfaxiaofeiduanxianchengchimoxing

ExchangeCodec:dubbo的编解码器实现,父类ExchangeCodec主要提供了编解码的操作,分别是编码请求(消费者)、编码响应(提供者)、解码请求(提供者)、解码响应(消费者),dubbo协议具体内容:前十六个bit是魔法字符0xdabb,可类比JAVA中的CAFEBABY,基础安全校验使用,16个bit,数据包类型:0:response,1:request,17个bit是调用方式,0:单向调用;1:双向调用.18是事件标志,0:请求、响应包;1:心跳,1923是标识序列化的机制,2是Hessian2…,五个字节记录,最多32个肯定够用了,2431数据的状态,256个数字可以看出dubbo留出了比较多的字节用于后续的扩展,20为OK状态,3295请求编号,这八个字节用来存储当前RPC请求的唯一标识,根据IP+端口+请求ID就可以确定唯一的请求,96127是消息体长度:占用4个字节,dubbo默认是8M,最大是16M(20+4=27个字节),所以使用了4个字节,后续就是请求的数据体内容了.

DubboCodec:解码数据,获取请求ID,获取请求类型标志位,若是响应类型,则说明是消费者端发送到提供者端的请求,状态码是正常则判断是心跳、还是常规请求,来执行相关逻辑,当时请求类型,再根据URL中的配置决定解码操作是在IO线程中还是在dubbo业务线程中执行,默认是在I/O线程中执行;若是请求类型,说明是提供者端接收到请求需要处理,大体逻辑和请求差不多.
编码请求(消费者端):框架版本、调用接口、写入接口指定的版本、方法名称、方法参数类型,方法参数值,
编码响应(提供者端):返回一个Byte记录响应码(响应异常、响应正常、响应空数据、响应异常带隐式参数、响应正常带隐式参数、响应空值带隐式参数),若允许隐式参数的传递,将参数一并返回给上游.

半包粘包问题:dubbo通过指定字符区分的,也可以认为是用的netty中的LengthFieldBasedFrameDecoder,原理差不多,首先基础的魔数字符校验,在获取到当前请求的data长度,若当前流达到该长度则读取到指定的下标,这个数据就是一条完整的RPC数据,后面的字符自然就是魔数开头了,当未达到该长度时,等待后续流数据的读取,再进行解析,最大长度不可超过8M,dubbo中还包含隐式参数、URL、参数类型等数据的传输,所以真实的数据一般在接近8M或者7M内比较合适.

Serialize

ObjectOutput、Serialization、ObjectInout
序列化层。如果数据要通过网络进行发送,则需要先做序列化,变成二进制流。序列化 层负责管理整个框架网络传输时的序列化/反序列化工作
默认是Hessian2序列化机制

总体调用过程

首先,调用过程也是从一个Proxy开始的,Proxy持有了一个Invoker对象。然后触发invoke
调用。在invoke调用过程中,需要使用Cluster,
Cluster负责容错,如调用失败的重试。Cluster在调用之前会通过Directory获取所有可以调用的远程服务Invoker列表(一个接口可能有多个节点提供服务)。由于可以调用的远程服务有很多,此时如果用户配置了路由规则(如指定某些
方法只能调用某个节点),那么还会根据路由规则将Invoker列表过滤一遍。

然后,存活下来的Invoker可能还会有很多,此时要调用哪一个呢?于是会继续通过
LoadBalance方法做负载均衡,最终选出一个可以调用的Invokero这个Invoker在调用之前又会
经过一个过滤器链,这个过滤器链通常是处理上下文、限流、计数等。

接着,会使用Client做数据传输,如我们常见的Netty Client等。传输之前肯定要做一些私
有协议的构造,此时就会用到Codec接口。构造完成后,就对数据包做序列化(Serialization),
然后传输到服务提供者端。服务提供者收到数据包,也会使用Codec处理协议头及一些半包、 粘包等。处理完成后再对完整的数据报文做反序列化处理。

随后,这个Request会被分配到线程池(ThreadPool)中进行处理Server会处理这些Request,
根据请求查找对应的Exporter (它内部持有了 Invoker) Invoker是被用装饰器模式一层一层套
了非常多Filter的,因此在调用最终的实现类之前,又会经过一个服务提供者端的过滤器链。
最终,我们得到了具体接口的真实实现并调用,再原路把结果返回。 至此,一个完整的远程调用过程就结束了

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

旺仔丷

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

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

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

打赏作者

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

抵扣说明:

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

余额充值