dubbo的集群容错

在这里插入图片描述

Cluster层

在维服务环境中,为了保证服务的高可用,服务通常都是以集群方式出现,很少出现单点服务。服务不是每时每刻都保持良好运行,如网络抖动、服务短暂不可用等,需要集群进行自动容错。在本地测试、服务降级,则需要Mock返回结果

通过Cluster的扩展点实现容错机制

mock=org.apache.dubbo.rpc.cluster.support.wrapper.MockClusterWrapper
failover=org.apache.dubbo.rpc.cluster.support.FailoverCluster
failfast=org.apache.dubbo.rpc.cluster.support.FailfastCluster
failsafe=org.apache.dubbo.rpc.cluster.support.FailsafeCluster
failback=org.apache.dubbo.rpc.cluster.support.FailbackCluster
forking=org.apache.dubbo.rpc.cluster.support.ForkingCluster
available=org.apache.dubbo.rpc.cluster.support.AvailableCluster
mergeable=org.apache.dubbo.rpc.cluster.support.MergeableCluster
broadcast=org.apache.dubbo.rpc.cluster.support.BroadcastCluster
zone-aware=org.apache.dubbo.rpc.cluster.support.registry.ZoneAwareCluster
  • Failover 失败自动切换重试,可用通过retries="2"设置重试次数。这是dubbo的默认容错机制,会对请求做负载均衡。通常使用在读操作或冥等的写操作上,但重试会导致接口的延迟增长,在下游机器负载达到极限时,重试容易加重下游服务的负载
  • Failfast 快速失败。当请求失败后,快速返回异常结果,不做任何重试。该容错机制会对请求做负载均衡,通常用在非冥等接口的调用上。受网络抖动的影响比较大
  • Failsafe 失败安全,当出现异常时,直接忽略异常,会对请求做负载均衡。通常使用在”佛系"调用场景,即不关心调用是否成功,并且不想抛出异常影响外层调用,通常用在如不重要的日志同步,即使出现异常也无所谓
  • Failback 失败自动恢复,请求失败后,会自动记录在失败队列中,并由一个定时线程定时重试,适用于一些异步或最终一致性的请求。请求会做负载均衡
  • Forking 并行调用多个服务提供者,只要其中一个返回,则立即返回结果。用户可以配置forks=“最大并行调用数"参数来确定最大并行调用的服务数量。通常使用在对接口实时性要求极高的调用上,但会浪费更多的资源
  • Broadcast 广播调用所有的服务,任意一个节点报错则报错。由于是广播,因此请求不需要做负载均衡。通常用户服务状态更新后的广播
  • Mock 广播调用所有可用的服务,任意一个节点报错则报错
  • Available 最简单的方式,请求不会做负载均衡,遍历所有的服务列表,找到第一个可用的节点,直接请求并返回结果。如果没有可用的节点,则直接跑出异常
  • Mergeable 可以自动把多个节点请求得到的结果进行合并

AbstractClusterInvoker

public Result invoke(final Invocation invocation) throws RpcException {
    checkWhetherDestroyed();
    // 绑定attachments 到 invocation中
    Map<String, String> contextAttachments = RpcContext.getContext().getAttachments();
    if (contextAttachments != null && contextAttachments.size() != 0) {
        ((RpcInvocation) invocation).addAttachments(contextAttachments);
    }
	// 列举 Invoker
    List<Invoker<T>> invokers = list(invocation);
    // 加载 loadBalance
    LoadBalance loadbalance = initLoadBalance(invokers, invocation);
    RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
    // 调用 doInvoke进行后续
    return doInvoke(invocation, invokers, loadbalance);
}
// 由容错策略实现的
protected abstract Result doInvoke(Invocation invocation, List<Invoker<T>> invokers,
                                   LoadBalance loadbalance) throws RpcException;
protected Invoker<T> select(LoadBalance loadbalance, Invocation invocation,
                                List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {
        if (CollectionUtils.isEmpty(invokers)) {
            return null;
        }
        // 获取调用方法名
        String methodName = invocation == null ? StringUtils.EMPTY_STRING : invocation.getMethodName();
  		// 获取 sticky 配置,sticky 表示粘滞连接。所谓粘滞连接是指让服务消费者尽可能的
    	// 调用同一个服务提供者,除非该提供者挂了再进行切换
        boolean sticky = invokers.get(0).getUrl()
                .getMethodParameter(methodName, CLUSTER_STICKY_KEY, DEFAULT_CLUSTER_STICKY);
        // 检测 invokers 列表是否包含 stickyInvoker,如果不包含,
        // 说明 stickyInvoker 代表的服务提供者挂了,此时需要将其置空
        if (stickyInvoker != null && !invokers.contains(stickyInvoker)) {
            stickyInvoker = null;
        }
 		// 在 sticky 为 true,且 stickyInvoker != null 的情况下。如果 selected 包含 
        // stickyInvoker,表明 stickyInvoker 对应的服务提供者可能因网络原因未能成功提供服务。
        // 但是该提供者并没挂,此时 invokers 列表中仍存在该服务提供者对应的 Invoker。
        if (sticky && stickyInvoker != null && (selected == null || !selected.contains(stickyInvoker))) {
           // availablecheck 表示是否开启了可用性检查,如果开启了,则调用 stickyInvoker 的 
            // isAvailable 方法进行检查,如果检查通过,则直接返回 stickyInvoker。
            if (availablecheck && stickyInvoker.isAvailable()) {
                return stickyInvoker;
            }
        }
		// availablecheck 表示是否开启了可用性检查,如果开启了,则调用 stickyInvoker 的 
        // isAvailable 方法进行检查,如果检查通过,则直接返回 stickyInvoker。
        Invoker<T> invoker = doSelect(loadbalance, invocation, invokers, selected);
    	// 如果 sticky 为 true,则将负载均衡组件选出的 Invoker 赋值给 stickyInvoker
        if (sticky) {
            stickyInvoker = invoker;
        }
        return invoker;
    }

select 方法的主要逻辑集中在了对粘滞连接特性的支持上。首先是获取 sticky 配置,然后再检测 invokers 列表中是否包含 stickyInvoker,如果不包含,则认为该 stickyInvoker 不可用,此时将其置空。这里的 invokers 列表可以看做是存活着的服务提供者列表,如果这个列表不包含 stickyInvoker,那自然而然的认为 stickyInvoker 挂了,所以置空。如果 stickyInvoker 存在于 invokers 列表中,此时要进行下一项检测 — 检测 selected 中是否包含 stickyInvoker。如果包含的话,说明 stickyInvoker 在此之前没有成功提供服务(但其仍然处于存活状态)。此时我们认为这个服务不可靠,不应该在重试期间内再次被调用,因此这个时候不会返回该 stickyInvoker。如果 selected 不包含 stickyInvoker,此时还需要进行可用性检测,比如检测服务提供者网络连通性等。当可用性检测通过,才可返回 stickyInvoker,否则调用 doSelect 方法选择 Invoker。如果 sticky 为 true,此时会将 doSelect 方法选出的 Invoker 赋值给 stickyInvoker

 private Invoker<T> doSelect(LoadBalance loadbalance, Invocation invocation,
                                List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {

        if (CollectionUtils.isEmpty(invokers)) {
            return null;
        }
        if (invokers.size() == 1) {
            return invokers.get(0);
        }
        // 通过负载均衡 选择Invoker
        Invoker<T> invoker = loadbalance.select(invokers, getUrl(), invocation);
        // 如果selected 包含负载均衡选择出Invoker,或者该Invoker无法经过可用性检查,此时进行重选
        if ((selected != null && selected.contains(invoker))
                || (!invoker.isAvailable() && getUrl() != null && availablecheck)) {
            try {
             	// 进行重选
                Invoker<T> rInvoker = reselect(loadbalance, invocation, invokers, selected, availablecheck);
                if (rInvoker != null) {
                    invoker = rInvoker;
                } else {
                    // 如果 rinvoker 不为空,则将其赋值给 invoker
                    int index = invokers.indexOf(invoker);
                    try {
                        // 获取 index + 1 位置处的 Invoker,以下代码等价于:
                   		// invoker = invokers.get((index + 1) % invokers.size());
                        invoker = invokers.get((index + 1) % invokers.size());
                    } catch (Exception e) {
                        logger.warn(e.getMessage() + " may because invokers list dynamic change, ignore.", e);
                    }
                }
            } catch (Throwable t) {
                logger.error(...);
            }
        }
        return invoker;
    }

FailoverClusterInvoker

FailoverClusterInvoker 在调用失败时,会自动切换 Invoker 进行重试。默认配置下,Dubbo 会使用这个类作为缺省 Cluster Invoker

   @SuppressWarnings({"unchecked", "rawtypes"})
    public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        List<Invoker<T>> copyInvokers = invokers;
        checkInvokers(copyInvokers, invocation);
        String methodName = RpcUtils.getMethodName(invocation);
        // 获取重试次数
        int len = getUrl().getMethodParameter(methodName, RETRIES_KEY, DEFAULT_RETRIES) + 1;
        if (len <= 0) {
            len = 1;
        }
        RpcException le = null; // last exception.
        List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyInvokers.size()); // invoked invokers.
        Set<String> providers = new HashSet<String>(len);
        // 循环调用,失败重试
        for (int i = 0; i < len; i++) {
            //Reselect before retry to avoid a change of candidate `invokers`.
            //NOTE: if `invokers` changed, then `invoked` also lose accuracy.
            if (i > 0) {
                checkWhetherDestroyed();
                // 在进行重试前重新列举Invoker,这些的好处是,如果某个服务挂了
                // 通过调用list可得到最新可用的Invoker列表
                copyInvokers = list(invocation);
                // 重建检查
                checkInvokers(copyInvokers, invocation);
            }
            // 通过负载均衡选择Invoker
            Invoker<T> invoker = select(loadbalance, invocation, copyInvokers, invoked);
            // 添加到invoker到invoked列表中,记录已经调用的invoker 失败自动切换
            invoked.add(invoker);
            // 设置invoked 到 RPC 上下文中
            RpcContext.getContext().setInvokers((List) invoked);
            try {
            	// 调用目标Invoker的invoke方法
                Result result = invoker.invoke(invocation);
                if (le != null && logger.isWarnEnabled()) {
                    logger.warn(...);
                }
                return result;
            } catch (RpcException e) {
                if (e.isBiz()) { // 业务异常,直接抛出
                    throw e;
                }
                le = e;
            } catch (Throwable e) {
                le = new RpcException(e.getMessage(), e);
            } finally {
                providers.add(invoker.getUrl().getAddress());
            }
        }
        // 若重试失败 ,则抛出异常
        throw new RpcException(...);
    }

FailfastClusterInvoker

FailfastClusterInvoker 只会进行一次调用,失败后立即抛出异常。适用于幂等操作,比如新增记录。

 public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
      checkInvokers(invokers, invocation);
      // 选择Invoker
      Invoker<T> invoker = select(loadbalance, invocation, invokers, null);
      try {
      	  // 调用Invoker
          return invoker.invoke(invocation);
      } catch (Throwable e) {
          if (e instanceof RpcException && ((RpcException) e).isBiz()) { // biz exception.
              throw (RpcException) e;
          }
          // 失败,直接抛出异常
          throw new RpcException(...);
      }
  }

FailsafeClusterInvoker

FailsafeClusterInvoker 是一种失败安全的 Cluster Invoker。所谓的失败安全是指,当调用过程中出现异常时,FailsafeClusterInvoker 仅会打印异常,而不会抛出异常。适用于写入审计日志等操作

public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
     try {
         checkInvokers(invokers, invocation);
         // 选择 Invoker
         Invoker<T> invoker = select(loadbalance, invocation, invokers, null);
         // 进行远程调用
         return invoker.invoke(invocation);
     } catch (Throwable e) {
     	 // 打印错误日志,但不抛出
         logger.error("Failsafe ignore exception: " + e.getMessage(), e);
         // 返回空结果忽略错误
         return AsyncRpcResult.newDefaultAsyncResult(null, null, invocation); // ignore
     }
 }

FailbackClusterInvoker

FailbackClusterInvoker 会在调用失败后,返回一个空结果给服务消费者。并通过定时任务对失败的调用进行重传,适合执行消息通知等操作

public class FailbackClusterInvoker<T> extends AbstractClusterInvoker<T> {
    private static final long RETRY_FAILED_PERIOD = 5; // 默认衰减重试5次
    private final int retries; // 重试次数 
    private final int failbackTasks;
    private volatile Timer failTimer; // 定时任务,对失败重新调用 衰减重试 
    public FailbackClusterInvoker(Directory<T> directory) {
        super(directory);
        int retriesConfig = getUrl().getParameter(RETRIES_KEY, DEFAULT_FAILBACK_TIMES);
        if (retriesConfig <= 0) {
            retriesConfig = DEFAULT_FAILBACK_TIMES;
        }
        int failbackTasksConfig = getUrl().getParameter(FAIL_BACK_TASKS_KEY, DEFAULT_FAILBACK_TASKS);
        if (failbackTasksConfig <= 0) {
            failbackTasksConfig = DEFAULT_FAILBACK_TASKS;
        }
        retries = retriesConfig;
        failbackTasks = failbackTasksConfig;
    }
    private void addFailed(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, Invoker<T> lastInvoker) {
        if (failTimer == null) {
            synchronized (this) {
                if (failTimer == null) {
                	// HashedWheelTimer dubbo的时间轮方式
                    failTimer = new HashedWheelTimer(
                            new NamedThreadFactory("failback-cluster-timer", true),
                            1,
                            TimeUnit.SECONDS, 32, failbackTasks);
                }
            }
        }
        // 重试
        RetryTimerTask retryTimerTask = new RetryTimerTask(loadbalance, invocation, invokers, lastInvoker, retries, RETRY_FAILED_PERIOD);
        try {
            failTimer.newTimeout(retryTimerTask, RETRY_FAILED_PERIOD, TimeUnit.SECONDS);
        } catch (Throwable e) {
            logger.error(...);
        }
    }
    @Override
    protected Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        Invoker<T> invoker = null;
        try {
            checkInvokers(invokers, invocation);
            // 选择Invoker
            invoker = select(loadbalance, invocation, invokers, null);
            // 进行调用
            return invoker.invoke(invocation);
        } catch (Throwable e) {
        	// 如果调用过程中发生异常,此时仅打印错误日志,不抛出异常
            logger.error(...);
            // 记录调用信息
            addFailed(loadbalance, invocation, invokers, invoker);
            // 返回一个空结果给服务消费者
            return AsyncRpcResult.newDefaultAsyncResult(null, null, invocation); // ignore
        }
    }
 }

ForkingClusterInvoker

ForkingClusterInvoker 会在运行时通过线程池创建多个线程,并发调用多个服务提供者。只要有一个服务提供者成功返回了结果,doInvoke 方法就会立即结束运行。ForkingClusterInvoker 的应用场景是在一些对实时性要求比较高读操作(注意是读操作,并行写操作可能不安全)下使用,但这将会耗费更多的资源

   private final ExecutorService executor = Executors.newCachedThreadPool(
            new NamedInternalThreadFactory("forking-cluster-timer", true));
    @Override
    @SuppressWarnings({"unchecked", "rawtypes"})
    public Result doInvoke(final Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        try {
            checkInvokers(invokers, invocation);
            final List<Invoker<T>> selected;
            // 获取forks配置
            final int forks = getUrl().getParameter(FORKS_KEY, DEFAULT_FORKS);
            // 获取超时时间
            final int timeout = getUrl().getParameter(TIMEOUT_KEY, DEFAULT_TIMEOUT);
            // 如果forks配置,直接将invokers赋值给selected
            if (forks <= 0 || forks >= invokers.size()) {
                selected = invokers;
            } else {
            	// 循环选出 forks个Invoker,并添加到selected中
                selected = new ArrayList<>();
                for (int i = 0; i < forks; i++) {
                	// 选出Invoker
                    Invoker<T> invoker = select(loadbalance, invocation, invokers, selected);
                    if (!selected.contains(invoker)) {
                        selected.add(invoker);
                    }
                }
            }
            RpcContext.getContext().setInvokers((List) selected);
            final AtomicInteger count = new AtomicInteger();
            // 阻塞队列
            final BlockingQueue<Object> ref = new LinkedBlockingQueue<>();
            // 遍历selected列表
            for (final Invoker<T> invoker : selected) {
            	// 为每个Invoker创建一个执行线程
                executor.execute(() -> {
                    try {
                    	// 进行远程调用
                        Result result = invoker.invoke(invocation);
                        // 将结果添加到阻塞队列中
                        ref.offer(result);
                    } catch (Throwable e) {
                        int value = count.incrementAndGet();
                        // 仅在value大于等于select.size()时,才将异常
                        // 放入阻塞队列中 --> 都调不通才抛异常
                        if (value >= selected.size()) {
                            ref.offer(e);
                        }
                    }
                });
            }
            try {
            	// 从阻塞队列中取出远程调用结果
                Object ret = ref.poll(timeout, TimeUnit.MILLISECONDS);
                // 如果结果类型为Trowable,则抛出异常
                if (ret instanceof Throwable) {
                    Throwable e = (Throwable) ret;
                    throw new RpcException(...);
                }
                // 返回结果
                return (Result) ret;
            } catch (InterruptedException e) {
                throw new RpcException(...);
            }
        } finally {
            // clear attachments which is binding to current thread.
            RpcContext.getContext().clearAttachments();
        }
    }

BroadcastClusterInvokerBroadcastClusterInvoker

BroadcastClusterInvoker 会逐个调用每个服务提供者,如果其中一台报错,在循环调用结束后,BroadcastClusterInvoker 会抛出异常。该类通常用于通知所有提供者更新缓存或日志等本地资源信息

 public Result doInvoke(final Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        checkInvokers(invokers, invocation);
        RpcContext.getContext().setInvokers((List) invokers);
        RpcException exception = null;
        Result result = null;
        // 遍历Invoker列表,逐个调用
        for (Invoker<T> invoker : invokers) {
            try {
            	// 进行远程调用
                result = invoker.invoke(invocation);
            } catch (RpcException e) {
            	// 保存异常,但会继续循环调用
                exception = e;
                logger.warn(e.getMessage(), e);
            } catch (Throwable e) {
                exception = new RpcException(e.getMessage(), e);
                logger.warn(e.getMessage(), e);
            }
        }
        // 如果有异常情况,会直接抛出
        if (exception != null) {
            throw exception;
        }
        return result;
    }

AvailableClusterInvoker

找到第一个可用的服务直接调用,并返回结果

 @Override
    public Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
    	// 遍历invokers列表,如果有Invoker是可用的,直接调用并返回
        for (Invoker<T> invoker : invokers) {
            if (invoker.isAvailable()) {
                return invoker.invoke(invocation);
            }
        }
        // 遍历列表未找到可用的Invoker,抛出异常
        throw new RpcException("No provider available in " + invokers);
    }

ClusterInterceptor 集群调用的拦截器

META-INF/dubbo/internal/org.apache.dubbo.rpc.cluster.interceptor.ClusterInterceptor 扩展点

context=org.apache.dubbo.rpc.cluster.interceptor.ConsumerContextClusterInterceptor
zone-aware=org.apache.dubbo.rpc.cluster.interceptor.ZoneAwareClusterInterceptor

ClusterInterceptor扩展点接口

@SPI
public interface ClusterInterceptor {
    void before(AbstractClusterInvoker<?> clusterInvoker, Invocation invocation);

    void after(AbstractClusterInvoker<?> clusterInvoker, Invocation invocation);
    default Result intercept(AbstractClusterInvoker<?> clusterInvoker, Invocation invocation) throws RpcException {
        return clusterInvoker.invoke(invocation);
    }

    interface Listener {
        void onMessage(Result appResponse, AbstractClusterInvoker<?> clusterInvoker, Invocation invocation);
        void onError(Throwable t, AbstractClusterInvoker<?> clusterInvoker, Invocation invocation);
    }
}

实现拦截逻辑

public abstract class AbstractCluster implements Cluster {
	// 构建拦截器链路
    private <T> Invoker<T> buildClusterInterceptors(AbstractClusterInvoker<T> clusterInvoker, String key) {
        AbstractClusterInvoker<T> last = clusterInvoker;
        // 通过扩展点加载所有的Interceptor
        List<ClusterInterceptor> interceptors = ExtensionLoader.getExtensionLoader(ClusterInterceptor.class).getActivateExtension(clusterInvoker.getUrl(), key);
        if (!interceptors.isEmpty()) {
            for (int i = interceptors.size() - 1; i >= 0; i--) {
                final ClusterInterceptor interceptor = interceptors.get(i);
                final AbstractClusterInvoker<T> next = last;
                last = new InterceptorInvokerNode<>(clusterInvoker, interceptor, next);
            }
        }
        return last;
    }

    @Override
    public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
        return buildClusterInterceptors(doJoin(directory), directory.getUrl().getParameter(REFERENCE_INTERCEPTOR_KEY));
    }
    @Override
    public Result invoke(Invocation invocation) throws RpcException {
            Result asyncResult;
            try {
                interceptor.before(next, invocation);
                asyncResult = interceptor.intercept(next, invocation);
            } catch (Exception e) {
                // onError callback
                if (interceptor instanceof ClusterInterceptor.Listener) {
                    ClusterInterceptor.Listener listener = (ClusterInterceptor.Listener) interceptor;
                    listener.onError(e, clusterInvoker, invocation);
                }
                throw e;
            } finally {
                interceptor.after(next, invocation);
            }
            return asyncResult.whenCompleteWithContext((r, t) -> {
                // onResponse callback
                if (interceptor instanceof ClusterInterceptor.Listener) {
                    ClusterInterceptor.Listener listener = (ClusterInterceptor.Listener) interceptor;
                    if (t == null) {
                        listener.onMessage(r, clusterInvoker, invocation);
                    } else {
                        listener.onError(t, clusterInvoker, invocation);
                    }
                }
            });
        }
        @Override
        public void destroy() {
            clusterInvoker.destroy();
        }
        @Override
        public String toString() {
            return clusterInvoker.toString();
        }
        @Override
        protected Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
            return null;
        }
    }

ConsumerContextClusterInterceptor

消费端集群调用,处理上下文的拦截器

public class ConsumerContextClusterInterceptor implements ClusterInterceptor, ClusterInterceptor.Listener {
    @Override
    public void before(AbstractClusterInvoker<?> invoker, Invocation invocation) {
        RpcContext.getContext()
                .setInvocation(invocation)
                .setLocalAddress(NetUtils.getHostAddress(), 0);
        if (invocation instanceof RpcInvocation) {
            ((RpcInvocation) invocation).setInvoker(invoker);
        }
        RpcContext.removeServerContext();
    }
    @Override
    public void after(AbstractClusterInvoker<?> clusterInvoker, Invocation invocation) {
        RpcContext.removeContext();
    }
    @Override
    public void onMessage(Result appResponse, AbstractClusterInvoker<?> invoker, Invocation invocation) {
        RpcContext.getServerContext().setAttachments(appResponse.getAttachments());
    }
    @Override
    public void onError(Throwable t, AbstractClusterInvoker<?> invoker, Invocation invocation) {
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值