Ribbon

Ribbon 实现负载均衡的接口以及内置的负载均衡规则

在这里插入图片描述

BaseLoadBalancer 的负载均衡规则是轮询。任何一个负载均衡器的规则由 IRule接口的实现类的 choose() 方法来定义。

常见的实现类:

在这里插入图片描述

  • RoundRobinRule:默认的规则,直接轮训 Server List
  • AvailabilityFilteringRule:考察服务器的可用性来调用;
  • WeightedResponseTimeRule:以响应时间作为权重;
  • ZoneAvoidanceRule:根据区域来调用;
  • BestAvailableRule:直接忽略连接失败的服务器,再去找并发量较低的服务器;
  • RandomRule:随机访问;
  • RetryRule:提供重试机制,即访问失败后重新找一个;

也可以自定义一个规则。

Ribbon 的主打功能就是负载均衡,其他都是次要的。看源码的重点就要放在负载机会机制上面。

Ribbon 判断服务器是否存活的接口

负载均衡器,LoadBalancer 需要定时 ping Server List 里面的服务器,确保它们存活。ping 操作的时间间隔定义在 ILoadBalancergetPingInterval() 方法,是否存活的标准可以在 IPing 的实现类的 isAlive() 方法中定义。

常见的 IPing 实现类:

在这里插入图片描述

RibbonEureka 简单得多,直接在 Spring Cloud环境下看源码即可。

Ribbon 实现负载均衡的大概流程

@LoadBalanced
@Bean
public RestTemplate getRestTemplate() {
	return new RestTemplate();
}

在代码中注入这个 Bean,使用它发送 HTTP 请求就会自动实现负载均衡。

在这里插入图片描述

Ribbon 原理:以 @LoadBalanced 注解作为入口

@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
public @interface LoadBalanced {
}

标记一个 RestTemplate 的 bean,这个 bean 会使用 LoadBalancerClient 通信。

根据流程图找到自动配置类:LoadBalancerAutoConfiguration

@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {

  // 前面自己使用@LoadBalanced标记的 RestTemplate Bean 会放入这个list
  // 至于怎么放入的先别管
  @LoadBalanced
  @Autowired(required = false)
  private List<RestTemplate> restTemplates = Collections.emptyList();

  @Bean
  public SmartInitializingSingleton loadBalancedRestTemplateInitializer
    (final List<RestTemplateCustomizer> customizers) {
    // 遍历 restTemplates 中的 RestTemplate 对象,
    // 调用每个 RestTemplateCustomizer 的 customize() 方法进行定制
    return new SmartInitializingSingleton() {
      @Override
      public void afterSingletonsInstantiated() {
        for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
          for (RestTemplateCustomizer customizer : customizers) {
            customizer.customize(restTemplate);
          }
        }
      }
    };
  }

  @Configuration
  @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
  static class LoadBalancerInterceptorConfig {
    // 负载均衡拦截器 LoadBalancerInterceptor
    @Bean
    public LoadBalancerInterceptor ribbonInterceptor(
      LoadBalancerClient loadBalancerClient,
      LoadBalancerRequestFactory requestFactory) {
      return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
    }

    @Bean
    @ConditionalOnMissingBean
    // RestTemplate 定制器
    public RestTemplateCustomizer restTemplateCustomizer(
      final LoadBalancerInterceptor loadBalancerInterceptor) {
      
      // 调用RestTemplate提供的方法发起HTTP请求时,
      // 会先经过一些拦截器
      return new RestTemplateCustomizer() {
        @Override
        public void customize(RestTemplate restTemplate) {
          List<ClientHttpRequestInterceptor> list = new ArrayList<>(
            restTemplate.getInterceptors());
          // 将 LoadBalancerInterceptor 添加到拦截器列表
          list.add(loadBalancerInterceptor);
          restTemplate.setInterceptors(list);
        }
      };
    }
  }

  ......
}

@LoadBalanced 的原理:

在这里插入图片描述

Ribbon 原理:Spring Cloud 通过负载均衡拦截器来改变 RestTemplate 的行为

调用 RestTemplate 的时候:restTemplate.getForObject("http://ServiceA/hello/"+name,String.class),会在底层封装好一个 Request 对象,包含请求的 URL、参数、期望响应结果的类型等等,然后经过一系列的拦截器,其中就有 LoadBalancerInterceptor

LoadBalancerInterceptorintercept() 方法:

@Override                               // RestTemplate 封装好的请求对象       // 请求体
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
                                    // Spring底层负责HTTP通信的组件
                                    final ClientHttpRequestExecution execution) throws IOException {
  final URI originalUri = request.getURI();
  String serviceName = originalUri.getHost();
  // Assert...
  // private LoadBalancerClient loadBalancer,构造器中设置的
	// 所以,通过拦截器,将本来应该由 RestTemplate 发送的请求变成 LoadBalancerClient
  // 回顾一下 @LoadBalanced 的注释
  return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}

拦截器会将 RestTemplate 构造的 HttpRequest 对象封装成 LoadBalancerRequest 对象,然后发送请求。

在这里插入图片描述

Ribbon 原理:LoadBalancerClient 实现负载均衡的流程

Ribbon 的自动装配类 RibbonAutoConfiguration

// 表示必须在 EurekaClientAutoConfiguration 被装配完毕之后才可以装配 RibbonAutoConfiguration
@AutoConfigureAfter(
    name = {"org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration"}
)
@Configuration
public class RibbonAutoConfiguration {
    // 用于发送 HTTP 请求的组件:LoadBalancerClient
    @Bean
    @ConditionalOnMissingBean({LoadBalancerClient.class})
    public LoadBalancerClient loadBalancerClient() {
                  // 实现类
        return new RibbonLoadBalancerClient(this.springClientFactory());
    }	
}

发送 HTTP 请求的时候调用 RibbonLoadBalancerClientexecute() 方法:

public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
  ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
  // 关键代码:选择合适的机器
  Server server = this.getServer(loadBalancer);
  if (server == null) {
    throw new IllegalStateException("No instances available for " + serviceId);
  } else {
    RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
    return this.execute(serviceId, ribbonServer, request);
  }
}

getServer 源码:loadBalancer 中包含 IRule 组件,负责定义实现负载均衡的逻辑

protected Server getServer(ILoadBalancer loadBalancer) {
  return loadBalancer == null ? null : loadBalancer.chooseServer("default");
}

Ribbon:如何获取 ILoadBalancer 对象

RibbonLoadBalancerClientexecute() 方法中:

ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId),获取到一个 LoadBalancer

源码:

protected ILoadBalancer getLoadBalancer(String serviceId) { // serviceId 要调用的服务名称
  // private SpringClientFactory clientFactory
  // 构造RibbonLoadBalancerClient的时候作为参数传入
  return this.clientFactory.getLoadBalancer(serviceId);
}

跟进去最终调用这个方法:在NamedContextFactory类中

// name=serviceId, type=ILoadBalancer.class
public <T> T getInstance(String name, Class<T> type) {
  AnnotationConfigApplicationContext context = getContext(name);
  if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
                                                          type).length > 0) {
    return context.getBean(type);
  }
  return null;
}

SpringClientFactory的注释中说明:每个服务名都对应一个 Spring ApplicationContext。所以每个服务可以部署在多台机器上,即多个服务实例,这些服务实例对应同一个 ILoadBalancer对象。

Ribbon:ILoadBalancer 的bean创建

RibbonClientConfiguration 中:

@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
                                        ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
                                        IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
  
  if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
    return this.propertiesFactory.get(ILoadBalancer.class, config, name);
  }
  // 可见,Ribbon 默认创建的负载均衡器是 ZoneAwareLoadBalancer
  return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
                                     serverListFilter, serverListUpdater);
}

在 SpringBoot、SpringCloud 的体系中,一般某个关键组件对应的 bean 都会在 XxxAutoConfigurationXxxConfiguration中创建。

继承关系:

在这里插入图片描述

获取 ILoadBalancer 对象的流程:对 ServiceA 进行负载均衡访问

在这里插入图片描述

Ribbon:如何整合Eureka来获取注册表

负载均衡器 ILoadBalancer 对象控制对某个服务的负载均衡访问时,需要获取这个服务的各个实例,每个实例对应一个节点,这些实例的集合被封装成 BaseLoadBalancer 类的两个成员变量:

// 所有节点
@Monitor(name = PREFIX + "AllServerList", type = DataSourceType.INFORMATIONAL)
protected volatile List<Server> allServerList = Collections
  .synchronizedList(new ArrayList<Server>());
// 可用节点
@Monitor(name = PREFIX + "UpServerList", type = DataSourceType.INFORMATIONAL)
protected volatile List<Server> upServerList = Collections
  .synchronizedList(new ArrayList<Server>());

Ribbon 如何获取到这些信息 ?显然,通过 Eureka Server 来获取。

已知,Ribbon 默认创建的负载均衡器是 ZoneAwareLoadBalancer,在其父类 DynamicServerListLoadBalancer 的构造器中,调用了 restOfInit() 方法:

void restOfInit(IClientConfig clientConfig) {
  boolean primeConnection = this.isEnablePrimingConnections();
  // turn this off to avoid duplicated asynchronous priming done in BaseLoadBalancer.setServerList()
  this.setEnablePrimingConnections(false);
  // 启用并初始化学习新的服务器的功能
  // 猜测:应该是有新的服务实例加入时,可以感知到
  enableAndInitLearnNewServersFeature();
	
  // 更新服务器列表....
  // 跟进去,发现调用成员变量 volatile ServerList<T> serverListImpl 的 getUpdatedListOfServers()
  // 即更新服务器列表
  // serverListImpl在构造 DynamicServerListLoadBalancer 时被传入
  updateListOfServers();
  if (primeConnection && this.getPrimeConnections() != null) {
    this.getPrimeConnections()
      .primeConnections(getReachableServers());
  }
  this.setEnablePrimingConnections(primeConnection);
}

ServerList 接口:提供了全量获取、增量获取的服务器列表的方法

public interface ServerList<T extends Server> {
  public List<T> getInitialListOfServers();
  public List<T> getUpdatedListOfServers();   
}

ServerList 的 bean 什么时候放入 Spring 容器中?EurekaRibbonClientConfiguration 类:Eureka 和 Ribbon 整合类

@Bean
@ConditionalOnMissingBean
public ServerList<?> ribbonServerList(IClientConfig config, Provider<EurekaClient> eurekaClientProvider) {
  if (this.propertiesFactory.isSet(ServerList.class, serviceId)) {
    return this.propertiesFactory.get(ServerList.class, config, serviceId);
  }
  DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(
    config, eurekaClientProvider);
  // Ribbon 默认使用的 ServerList 的实现是 DomainExtractingServerList
  // 但 getInitialListOfServers()、getUpdatedListOfServers() 方法里面使用了 discoveryServerList
  // 然后进行一些封装
  DomainExtractingServerList serverList = new DomainExtractingServerList(
    discoveryServerList, config, this.approximateZoneFromHostname);
  return serverList;
}

Ribbon 默认的 ServerList 接口的实现类为 DomainExtractingServerList,但里面调用的是 DiscoveryEnabledNIWSServerList 实现的方法。

DiscoveryEnabledNIWSServerList也是通过服务的 EurekaClient 对象,根据每个服务实例的虚拟 IP 地址,获取到对应的 InstanceInfo 对象,封装成 Server 对象加入 List 中。

在这里插入图片描述

Ribbon 获取到注册表之后,如何持续更新

EurekaClient 对象默认每隔 30s 会获取增量注册表信息,所以 Ribbon 的负载均衡器肯定也会定期更新 server list

在构造默认的负载均衡器 ZoneAwareLoadBalancer 的时候,调用了父类的 restOfInit() 方法,里面:

enableAndInitLearnNewServersFeature();

当时猜测这个方法的作用为启用和初始化学习新服务器的功能,很可能与持续更新注册表有关:

protected volatile ServerListUpdater serverListUpdater;
protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
  @Override
  public void doUpdate() {
    updateListOfServers();
  }
};

public void enableAndInitLearnNewServersFeature() {
  // serverListUpdater对象被作为构造器的参数传入
  // 默认使用的实现类:PollingServerListUpdater
  serverListUpdater.start(updateAction);
}

start() 方法的源码:

@Override
public synchronized void start(final UpdateAction updateAction) {
  if (isActive.compareAndSet(false, true)) {
    // 创建一个用于增量获取注册表信息的线程
    final Runnable wrapperRunnable = new Runnable() {
      @Override
      public void run() {
        if (!isActive.get()) {
          if (scheduledFuture != null) {
            scheduledFuture.cancel(true);
          }
          return;
        }
        try {
          updateAction.doUpdate();
          lastUpdated = System.currentTimeMillis();
        } catch (Exception e) {
          logger.warn("Failed one update cycle", e);
        }
      }
    };
    // 调度
    scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
      wrapperRunnable,
      // 构造器中指定
      // 默认1000ms
      initialDelayMs,
      // 默认 30x1000ms
      refreshIntervalMs,
      TimeUnit.MILLISECONDS
    );
  } else {
    logger.info("Already active, no-op");
  }
}

所以,增量获取注册表信息的核心方法就是 ServerListUpdater.UpdateActiondoUpdate(),就是调用 ServerListgetUpdatedListOfServers() 来获取,而这个方法底层也是直接向 EurekaClient 对象拿到增量的注册表信息。

默认使用的 ServerList 实现为 DiscoveryEnabledNIWSServerList,这两个方法的实现:

@Override
public List<DiscoveryEnabledServer> getInitialListOfServers(){
  return obtainServersViaDiscovery();
}

@Override
public List<DiscoveryEnabledServer> getUpdatedListOfServers(){
  return obtainServersViaDiscovery();
}

初次获取与后续的更新一样,都是调用obtainServersViaDiscovery() 方法。

在这里插入图片描述

Ribbon 默认的负载均衡算法

回顾:每个服务,如ServiceA,都有一个 ApplicationContext,所以服务和负载均衡器为一一对应的关系,每个负载均衡器对象都包含一份 server list。要访问某个服务时,就要获取到该服务对应的server list,通过一定的算法,获取到某个服务实例(对应的机器,因为一个服务可能部署在多台机器),使对这个服务的请求均匀地打在不同的机器上。

在执行 RibbonLoadBalancerClientexecute() 方法之后,选择目标的服务实例的方法为 getServer()

protected Server getServer(ILoadBalancer loadBalancer) {
  // 这里的 loadBalancer 默认为 ZoneAwareLoadBalancer
  return loadBalancer == null ? null : loadBalancer.chooseServer("default");
}

这里先暂时不管 Zone 的概念,Zone 实际上就是机房,在 Ribbon 的设计中,如果一个服务部署在不同机房的机器,就不是一个服务对应一个 ILoadBalancer 对象了,而是每个机房一个。所以 ZoneAwareLoadBalancerchooseServer() 方法会获取到每个机房的负载均衡器,然后调用这些负载均衡器的 chooesServer() 方法来获取目标服务实例:

if (zone != null) {
  BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
  // key = Default
  server = zoneLoadBalancer.chooseServer(key);
}

原来最终还是使用了 BaseLoadBalancer 的实现,所以默认的负载均衡算法就是直接轮询 server list

public Server chooseServer(Object key) {
  if (counter == null) {
    counter = createCounter();
  }
  counter.increment();
  if (rule == null) {
    return null;
  } else {
    try {
      // protected IRule rule = DEFAULT_RULE;
      // private final static IRule DEFAULT_RULE = new RoundRobinRule();
      return rule.choose(key);
    } catch (Exception e) {
      logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
      return null;
    }
  }
}

负载均衡的算法被定义在 IRule 对象的choose()方法中,RoundRobinRule的实现为:

// 获取下一个被访问的服务实例在 server list 中的下标
int nextServerIndex = incrementAndGetModulo(serverCount);
// 获取出来
server = allServers.get(nextServerIndex);

计算下标的方法:就是记录一个原子变量,对 server list 的 size 取模之后,对其进行 CAS 设置的操作。

private int incrementAndGetModulo(int modulo) {
  for (;;) {
    // private AtomicInteger nextServerCyclicCounter
    int current = nextServerCyclicCounter.get();
    // modulo 为server list 的size
    int next = (current + 1) % modulo;
    if (nextServerCyclicCounter.compareAndSet(current, next))
      return next;
  }
}

所以,默认的负载均衡算法很简单,就是自增并取模,这样就达到了轮询 server list 的效果。

Ribbon 选取到 Server 对象如何发送请求

RibbonLoadBalancerClient 执行 execute() 方法,最终获取到 Server 对象之后,如何发送 HTTP 请求到这个服务实例?

public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
  ...
  // 封装成 RibbonServer,ServiceInstance的子类
  RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
  // 调用重载的方法
  return this.execute(serviceId, ribbonServer, request);
}

前面提及,我们通过 RestTemplate 发送请求的时候,会通过拦截器来改变它的行为,这个过程中,一个 HttpRequest 被封装成一个 LoadBalancerRequest 对象。那么,LoadBalancerRequest 是什么?

在 Ribbon 提供的负载均衡拦截器中:

@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
                                    final ClientHttpRequestExecution execution) throws IOException {
  final URI originalUri = request.getURI();
  String serviceName = originalUri.getHost();
                                                // 构造一个LoadBalancerRequest对象
  // 这里执行的 execute() 方法实际上就是调用了 LoadBalancerRequest 的 apply() 方法
  return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}

实际上,LoadBalancerRequest 是一个接口,这里只是构造一个匿名内部类来实现它:

public interface LoadBalancerRequest<T> {
  public T apply(ServiceInstance instance) throws Exception;
}

构造 LoadBalancerRequest 的过程:

public LoadBalancerRequest<ClientHttpResponse> createRequest(final HttpRequest request,
                                                             final byte[] body, final ClientHttpRequestExecution execution) {
  return new LoadBalancerRequest<ClientHttpResponse>() {
    // 用于发送请求
    @Override
    public ClientHttpResponse apply(final ServiceInstance instance)
      throws Exception {
      HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, loadBalancer);
      if (transformers != null) {
        for (LoadBalancerRequestTransformer transformer : transformers) {
          serviceRequest = transformer.transformRequest(serviceRequest, instance);
        }
      }                // 真正发生HTTP请求的组件:ClientHttpRequestExecution
      return execution.execute(serviceRequest, body);
    }

  };
}

大致流程:

在这里插入图片描述

Ribbon 使用 Ping 机制来判断服务实例是否存活有效吗

SpringCloud 整合 RibbonEureka 的场景下,注册了以下的 IPing 对象:

@Bean
@ConditionalOnMissingBean
public IPing ribbonPing(IClientConfig config) {
  if (this.propertiesFactory.isSet(IPing.class, name)) {
    return this.propertiesFactory.get(IPing.class, config, name);
  }
  return new DummyPing();
}

即在默认的情况下,注册了一个 DummyPing 的对象:

public class DummyPing extends AbstractLoadBalancerPing {
  public DummyPing() {
  }
  public boolean isAlive(Server server) {
    return true;
  }
  @Override
  public void initWithNiwsConfig(IClientConfig clientConfig) {
  }
}

然而这个 bean 里面什么也没有做,即默认的情况下,Ribbon 的负载均衡器并不会对 server list中的服务实例进行 ping 操作。因为 Eureka 会自行判断服务实例是否存活,并且有故障感知与下线的功能,而 EurekaClient 对象又会定期获取增量注册表,所以 server list 中的服务实例往往都是可用的,并不需要通过 ping 来验证。在开发中,也可以自定义一个 IPing 对象或者 Ribbon 提供的其他实现,如:NIWSDiscoveryPing

// 检查负载均衡器的 server list 中的 server 对应的InstanceInfo的状态
public boolean isAlive(Server server) {
  boolean isAlive = true;
  if (server!=null && server instanceof DiscoveryEnabledServer){
    DiscoveryEnabledServer dServer = (DiscoveryEnabledServer)server;	            
    InstanceInfo instanceInfo = dServer.getInstanceInfo();
    if (instanceInfo!=null){	                
      InstanceStatus status = instanceInfo.getStatus();
      if (status!=null){
        // UP 就表示存活
        isAlive = status.equals(InstanceStatus.UP);
      }
    }
  }
  return isAlive;
}

有一点需要注意:调度这个 PingTask 的定时任务时,传入的时间间隔 pingIntervalSeconds 不是默认的 10s。在RibbonClientConfiguration中注册ZoneAwareLoadBalancer的bean的时候,会调用到父类以下的构造器:

public BaseLoadBalancer(IClientConfig config, IRule rule, IPing ping) {
  initWithConfig(config, rule, ping);
}
void initWithConfig(IClientConfig clientConfig, IRule rule, IPing ping) {
  ...
    int pingIntervalTime = Integer.parseInt(""
                                            + clientConfig.getProperty(
                                              CommonClientConfigKey.NFLoadBalancerPingInterval,
                                              Integer.parseInt("30")));'
    // 设置时间间隔,并启动 PingTask 任务
    setPingInterval(pingIntervalTime);
  ...
}
public void setPingInterval(int pingIntervalSeconds) {
  if (pingIntervalSeconds < 1) {
    return;
  }
  this.pingIntervalSeconds = pingIntervalSeconds;
  ...
  // 启动 PingTask的任务,一秒后执行一次,以后每30s一次,对server list中的server执行一次isAlive()方法
  setupPingTask(); // since ping data changed
}

有的博客跟踪源码的时候,走到了错误的构造器,所以说成每10s一次。

Ribbon 默认的负载均衡算法存在的问题

默认负载均衡算法就是轮询,这种不考虑服务实例可用性的方式会存在一些问题:

在这里插入图片描述

所以,在默认的情况下,SereviceA peer2 宕机之后,ServiceB 最多可能在 180+30+30s 即 4 分钟之后才会将其从 peer2 中移除。但一般也不建议修改默认的负载均衡算法,这个问题应该留给 hystrix 的熔断机制去解决。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值