spring cloud 学习笔记1

认识springcloud

版本号

spring cloud 的 版本号 是用单词来命名的,和传统的命名规则不一致,每个单词的首字母顺序为 A B C D…。

eureka

服务发现与注册

搭建eureka服务

1.创建子模块ms_eureka
2.引入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

3.application.yml

server:
  port: 9000  
spring:
  application:
    name: ms-eureka
eureka:
  client:
    # 是否需要从eureka获取注册信息
    fetch-registry: false
    # 是否需要把该服务注册到eureka
    register-with-eureka: false

4.启动类添加注解@EnableEurekaServer

@SpringBootApplication
@EnableEurekaServer
public class MsEurekaApplication {
 public static void main(String[] args) {
  SpringApplication.run(MsEurekaApplication.class, args);
 }
}

为eureka客户端注册服务到eureka服务端

  1. 添加依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
  1. application.yml
eureka:
  client:
    service-url:
      defaultZone: http://localhost:9000/eureka/
  1. 启动类添加注解@EnableEurekaClient
@SpringBootApplication
@EnableEurekaClient
public class MsUserApplication {
 public static void main(String[] args) {
  SpringApplication.run(MsUserApplication.class, args);
 }
}

DiscoveryClient 实现电影微服务和用户微服务的解耦

@RestController
@RequestMapping("/order")
public class OrderController {
 @Autowired
 private RestTemplate restTemplate ; 
 @Autowired
 private DiscoveryClient discoveryClient ; 
 @GetMapping("/{userId}")
 public User order(@PathVariable int userId) {  
  List<ServiceInstance> serviceInstanceList = discoveryClient.getInstances("ms-user") ;
  ServiceInstance serviceInstance = serviceInstanceList.get(0) ;
  User user = restTemplate.getForObject("http://"+serviceInstance.getHost()+":"+serviceInstance.getPort()+"/user/"+userId, User.class) ;
  return user ;
 }
}

eureka集群实现

  1. application-cluster-1.yml
server:
  port: 9000    
eureka:
  client:
    # 是否需要从eureka获取注册信息
    fetch-registry: true
    # 是否需要把该服务注册到eureka
    register-with-eureka: true
    service-url:
      defaultZone: http://localhost:8000/eureka
  1. application-cluster-2.yml
server:
  port: 8000
eureka:
  client:
    # 是否需要从eureka获取注册信息
    fetch-registry: true
    # 是否需要把该服务注册到eureka
    register-with-eureka: true
    service-url:
      defaultZone: http://localhost:9000/eureka
  1. 启动两台集群eureka服务
    启动时添加参数
    -Dspring.profiles.active=cluster-1
    -Dspring.profiles.active=cluster-2

4.电影微服务和用户微服务注册到集群配置``

server:
  port: 9002  
spring:
  application:
    name: ms-movie
eureka:
  client:
    service-url:
      defaultZone: http://localhost:9000/eureka/,http://localhost:8000/eureka/
server:
  port: 9001  
spring:
  application:
    name: ms-user
  datasource:
    url: jdbc:mysql://192.168.195.135:3306/study?useUnicode=true&characterEncoding=utf8
    username: study
    password: study123
  jpa:
    show-sql: true
eureka:
  client:
    service-url:
      defaultZone: http://localhost:9000/eureka/,http://localhost:8000/eureka/

服务提供方

服务提供方要向eurekaServer注册服务,并完成续约等工作。

  1. 服务注册
eureka:
  client:
    # 是否需要把该服务注册到eureka
    register-with-eureka: true
  1. 服务注册后需要向eureka发送心跳
eureka:
  instance:
    # 服务失效的间隔时间默认90秒
    lease-expiration-duration-in-seconds: 90
    # 服务续约的间隔时间默认30秒
    lease-renewal-interval-in-seconds: 30

服务消费方

  1. 获取服务注册信息
eureka:
  client:
    # 是否需要从eureka获取注册信息
    fetch-registry: true
  1. 更新注册信息
eureka:
  client:
    #刷新注册信息的间隔时间默认30s
    registry-fetch-interval-seconds: 30

eureka服务失效剔除和自我保护

  1. 失效剔除
    默认情况下每隔60s对失效的服务(默认90s未续约的服务)进行剔除,间隔时间配置参数为:
eureka:
  server:
    # 失效服务剔除间隔时间ms默认60s
    eviction-interval-timer-in-ms: 60000
  1. 自我保护
    eureka服务会统计15分钟内的心跳失败的服务实例是否超过15%。在生产情况下,由于网络原因,心跳失败,但其实服务可能是正常运行的,此时剔除掉是不恰当的做法,所以会进行保护起来,不予剔除,这种可以保证大多服务依然可用。
eureka:
  server:
    # 自我保护机制是否可用默认true
    enable-self-preservation: true

SpringCloud 服务调用与负载均衡

方式

  1. RestTemplate + Ribbon
  2. OpenFeign + 内置Ribbon ----推荐使用

负载均衡组件Ribbon

Ribbon是Netflix 发布的负载均衡组件,提供了负载均衡的算法,如轮询,随机等,也可以自定义算法。查看ms-movie的pom.xml文件发现spring-cloud-starter-netflix-eureka-client依赖包已经导入了ribbon的依赖。

  1. 负载均衡的调用方式
// 负载均衡客户端对象
@Autowired
 private LoadBalancerClient client ;
 /**
  * - 负载均衡的调用方式
  * @param userId
  * @return
  */
 @GetMapping("/{userId}")
 public User order(@PathVariable int userId) {
  // 使用Ribbon选择一台服务实例(默认算法为轮询)
  ServiceInstance serviceInstance = client.choose("ms-user") ;
  System.out.println(serviceInstance.getPort());
  // 使用restTemplate调用服务
  User user = restTemplate.getForObject("http://"+serviceInstance.getHost()+":"+serviceInstance.getPort()+"/user/"+userId, User.class) ;
  return user ;
 }
  1. 负载均衡的调用方式(简化版)
@Bean
 // 添加Ribbon负载均衡
 @LoadBalanced
 public RestTemplate restTemplate() {
  return new RestTemplate() ;
 }
@GetMapping("/{userId}")
 public User order(@PathVariable int userId) {
  // 使用restTemplate调用服务
  // 直接使用服务名(默认算法为轮询)
  User user = restTemplate.getForObject("http://ms-user/user/"+userId, User.class) ;
  return user ;
 }
  1. 轮询算法底层代码跟踪
    拦截器会拦截请求地址,获取真正的请求服务,使用%服务器数量来获取。

  2. 修改负载均衡算法

#修改ribbon负载均衡算法 (随机算法)
ms-user: 
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

使用OpenFeign实现服务调用

OpenFeign是Netflix开发的声明式,模板化的http客户端,帮助我们快捷的调用http rest 的api。OpenFeign已经默认集成了Ribbon负载均衡。

调用步骤

  1. movie服务调用端引入依赖
<!-- 导入OpenFeign依赖 -->
   <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
   </dependency>
  1. 启动类开启注解@EnableFeignClients
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class MsMovieApplication {
 public static void main(String[] args) {
  SpringApplication.run(MsMovieApplication.class, args);
 } 
// @Bean
// // 添加Ribbon负载均衡
// @LoadBalanced
// public RestTemplate restTemplate() {
//  return new RestTemplate() ;
// }
}
  1. 编写代理接口
// 使用OpenFeign客户端
// 实现ms-user服务的动态代理
@FeignClient("ms-user")
public interface MsUserClient {
 // 动态代理接口
 // 接口方法为ms-user的UserController的方法
 // @GetMapping("/user/{userId}") 路径应该完整
 // @PathVariable(value="userId") 中的value不能省略
 @GetMapping("/user/{userId}")
 public User findById(@PathVariable(value="userId") int userId) ;
}
  1. 使用代理接口调用远程服务方法
 @Autowired
 private MsUserClient msUserClient; 
 /**
  * - 负载均衡的调用方式(简化版)
  * @param userId
  * @return
  */
 @GetMapping("/{userId}")
 public User order(@PathVariable int userId) {
  // 使用restTemplate调用服务
  // 直接使用服务名
  User user = msUserClient.findById(userId) ;  
  return user ;
 }

熔断器Hystrix

Hystrix是Netflix开源的一个延迟和容错库,用于隔离访问远程服务、第三方库,防止出现级联失败(雪崩效应)

雪崩效应

服务提供者的不可用导致服务消费者不可用,并且逐渐放大的过程

状态

  1. 关闭 正常请求
  2. 打开 失败达到阈值后
  3. 半开 打开一段时间后放行部分请求

Ribbon添加Hystrix熔断器

修改电影微服务(调用方添加熔断器)

  1. 导入Hystrix的依赖包
<!-- 导入Hystrix依赖 -->
   <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
   </dependency>
  1. 使用@HystrixCommand声明fallback方法
  2. 编写fallback方法逻辑
    需要注意的是fallback方法的参数和返回类型必须与order方法的参数和返回类型一致,否则会报错。
 @GetMapping("/{userId}")
 @HystrixCommand(fallbackMethod="fallback")
 public User order(@PathVariable int userId) {
  // 使用restTemplate调用服务
  // 直接使用服务名
  User user = restTemplate.getForObject("http://ms-user/user/"+userId, User.class) ;
  return user ;
 }
 /**
  * 熔断器回调方法
  * @return
  */
 public User fallback(int userId) {
  System.out.println("falback....");
  return null ;
 }
  1. 在启动类添加@EnableHystrix注解

OpenFeign方式开启Hystrix(推荐使用)

  1. 添加依赖
  2. 开启配置
#默认OpenFeign的熔断器是关闭的,所以需要开启
feign:
  hystrix:
    enabled: true
  1. 编写fallback处理类,实现服务调用代理接口
/**
 * 熔断器fallback类的实现
 * @author eastqi
 *
 */
@Component
public class MsUserUserControllerImpl implements MsUserUserController {
 @Override
 public User findById(int userId) {
  User u = new User() ;
  u.setUserName("fallback...");
  return u;
 }
}
  1. 在服务调用代理接口中指定编写的实现类
    fallback=MsUserUserControllerImpl.class
// 使用OpenFeign客户端
// 实现ms-user服务的动态代理
// fallback 指定熔断器回调类
@FeignClient(value="ms-user",fallback=MsUserUserControllerImpl.class)
public interface MsUserUserController {
 // 动态代理接口
 // 接口方法为ms-user的UserController的方法
 // @GetMapping("/user/{userId}") 路径应该完整
 // @PathVariable(value="userId") 中的value不能省略
 @GetMapping("/user/{userId}")
 public User findById(@PathVariable(value="userId") int userId) ;
}

搭建Hystrix监控服务

  1. 创建ms_hystrix_monitor模块
  2. 引入相关依赖
   <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
   </dependency>
   <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
   </dependency>
   <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
   </dependency>
  1. 启动类开启@EnableHystrixDashboard
@SpringBootApplication
// 开启HystrixDashboard监控服务面板
@EnableHystrixDashboard
public class MsHystrixMonitorApplication {
 public static void main(String[] args) {
  SpringApplication.run(MsHystrixMonitorApplication.class, args);
 }
}

服务调用方暴露端点

可以参考网上其他文章,感觉用的不是很多

Spring Cloud 网关

作用:地址统一管理、权限控制、负载均衡

认识Spring Clound Zuul

zuul是Netflix开源的微服务网关,可以和Eureka、Ribbon、Hystrix等组件配合使用:
身份认证与安全
审查与监控
动态路由
压力测试
负载分配
静态响应处理
多区域弹性

Zuul动态路由实现步骤

  1. 创建独立的网关微服务模块:ms-gateway
  2. 导入zuul和eureka的依赖(网关本身也要注册到Eureka)
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
  1. 启动类添加@EnableZuulProxy
@SpringBootApplication
@EnableEurekaClient
// 开启网关代理功能
@EnableZuulProxy
public class MsGatewayApplication {
 public static void main(String[] args) {
  SpringApplication.run(MsGatewayApplication.class, args);
 }
}
  1. 配置application.yml路由规则
server:
  port: 80
spring:
  application:
    name: ms-gateway
eureka:
  client:
    service-url:
      defaultZone: http://localhost:9000/eureka/,http://localhost:8000/eureka/
  instance:
    prefer-ip-address: true
#zuul网关的动态路由配置
#如果path和service-id的名称是一致的,则下面可以不用配置
zuul:
  routes:
    ms-user:
      #需要转发的路径
      path: ms-user
      #转发到的服务名称
      service-id: ms-user
    ms-movie:
      path: ms-movie
      service-id: ms-movie
  1. 改成通过网关的端口来调用
    http://localhost:9002/order/1
    ===》
    http://localhost/ms-movie/order/1

Zuul动态路由负载均衡

通过上一步的Zuul动态路由配置,已经负载均衡,Zuul是通过Ribbon实现负载均衡,所以默认策略是轮询

Zuul过滤器

实际上动态路由功能在运行时,它的路由映射和请求转发都是由几个不同的Zuul过滤器完成的。其中,路由映射主要通过pre类型的过滤器完成,它将请求路径与配置的路由规则进行匹配,以找到需要转发的目标地址;而请求转发的部分则是由routel类型的过滤器来完成,对pre类型过滤器获得的路由地址进行转发。所以Zuul过滤器可以说是Zuul实现API网关功能最为核心的部件。

  1. Zuul过滤器方法说明
public class MyFileter1 extends ZuulFilter {
 /**
  * - 是否执行
  */
 @Override
 public boolean shouldFilter() {
  // TODO Auto-generated method stub
  return false;
 }
 /**
  * - 过滤器具体的逻辑
  */
 @Override
 public Object run() throws ZuulException {
  // TODO Auto-generated method stub
  return null;
 }
 /**
  * 过滤器类型
  * - pre 可以在请求被路由之前调用
  * - routing 在路由请求时候被调用
  * - post 在routing和error过滤器之后被调用
  * - error 处理请求时发生错误是被调用
  */
 @Override
 public String filterType() {
  // TODO Auto-generated method stub
  return null;
 }
 /**
  * -执行顺序,数值越小优先级越高
  */
 @Override
 public int filterOrder() {
  // TODO Auto-generated method stub
  return 0;
 }
}
  1. Zuul过滤器-生命周期
    在这里插入图片描述
  2. Zull过滤器的编写
@Component
public class MyFileter1 extends ZuulFilter {
 /**
  * - 是否执行
  */
 @Override
 public boolean shouldFilter() {
  return true;
 }
 /**
  * - 过滤器具体的逻辑
  */
 @Override
 public Object run() throws ZuulException {
  System.out.println("run myfileter1...");
  return null;
 }
 /**
  * 过滤器类型
  * - pre 可以在请求被路由之前调用
  * - routing 在路由请求时候被调用
  * - post 在routing和error过滤器之后被调用
  * - error 处理请求时发生错误是被调用
  */
 @Override
 public String filterType() {
  return FilterConstants.PRE_TYPE;
 }
 /**
  * -执行顺序,数值越小优先级越高
  */
 @Override
 public int filterOrder() {
  return 1;
 }
}
  1. Zuul过滤器实现权限认证
    zuul已经实现很多过滤器:
    在这里插入图片描述

自定义权限认证过滤器代码如下:

/**
 * - 权限认证过滤器
 * @author eastqi
 *
 */
@Component
public class AuthFileter extends ZuulFilter {
 /**
  * - 是否执行
  */
 @Override
 public boolean shouldFilter() {
  return true;
 }
 /**
  * - 过滤器具体的逻辑
  */
 @Override
 public Object run() throws ZuulException {
  RequestContext ctx = RequestContext.getCurrentContext();
  HttpServletRequest request = ctx.getRequest() ;
  HttpServletResponse response = ctx.getResponse() ;
  String token = request.getParameter("token") ;
  if(!"user".equals(token)) {
   // 不继续转发微服务,直接给用户响应
   ctx.setSendZuulResponse(false);
   response.setStatus(401);
   return null ;
  }
  return null;
 }
 /**
  * 过滤器类型
  * - pre 可以在请求被路由之前调用
  * - routing 在路由请求时候被调用
  * - post 在routing和error过滤器之后被调用
  * - error 处理请求时发生错误是被调用
  */
 @Override
 public String filterType() {
  return FilterConstants.PRE_TYPE;
 }
 /**
  * -执行顺序,数值越小优先级越高
  */
 @Override
 public int filterOrder() {
  return 0;
 }
}
  1. Zuul过滤器实现自定义异常处理
    默认的异常处理过滤器为SendErrorFilter

5.1 先将Zuul定义的过滤器失效,这样才能使用我们自定义的过滤器;因为异常处理过滤器只能有一个生效。

# 将Zuul默认的异常过滤器失效
zuul:
  SendErrorFilter:
    error:
      disable: true

5.2 编写代码

/**
 * - 自定义异常处理过滤器
 * @author eastqi
 *
 */
@Component
public class MyErrorFileter extends ZuulFilter {
 /**
  * - 是否执行
  */
 @Override
 public boolean shouldFilter() {
  return true;
 }
 /**
  * - 过滤器具体的逻辑
  */
 @Override
 public Object run() throws ZuulException {
  RequestContext ctx = RequestContext.getCurrentContext();
  // 获取异常信息
  Throwable throwable = ctx.getThrowable() ;
  HttpServletResponse response = ctx.getResponse() ;
  
  ResponseBean responseBean = new ResponseBean(false,throwable.getMessage()) ;
  
  ObjectMapper mapper = new ObjectMapper() ;
  
  try {
   String json = mapper.writeValueAsString(responseBean) ;
   response.setContentType("text/json;chatset=utf-8");
   response.getWriter().write(json);
  } catch (Exception e) {
   e.printStackTrace();
  }
  return null;
 }
 /**
  * 过滤器类型
  * - pre 可以在请求被路由之前调用
  * - routing 在路由请求时候被调用
  * - post 在routing和error过滤器之后被调用
  * - error 处理请求时发生错误是被调用
  */
 @Override
 public String filterType() {
  return FilterConstants.ERROR_TYPE;
 }
 /**
  * -执行顺序,数值越小优先级越高
  */
 @Override
 public int filterOrder() {
  return 0;
 }
}
  1. Zuul网关与swagger2整合
    6.1 gateway模块导入swagger2依赖
   <!-- 导入swagger依赖 -->
   <dependency>
     <groupId>io.springfox</groupId>
     <artifactId>springfox-swagger2</artifactId>
     <version>2.8.0</version>
 </dependency>
 <dependency>
     <groupId>io.springfox</groupId>
     <artifactId>springfox-swagger-ui</artifactId>
     <version>2.8.0</version>
 </dependency>

Zuul网关与swagger2整合

swagger2常用注解

常用注解: 
- @Api()用于类; 
表示标识这个类是swagger的资源 
- @ApiOperation()用于方法; 
表示一个http请求的操作 
- @ApiParam()用于方法,参数,字段说明; 
表示对参数的添加元数据(说明或是否必填等) 
- @ApiModel()用于类 
表示对类进行说明,用于参数用实体类接收 
- @ApiModelProperty()用于方法,字段 
表示对model属性的说明或者数据操作更改 
- @ApiIgnore()用于类,方法,方法参数 
表示这个方法或者类被忽略 
- @ApiImplicitParam() 用于方法 
表示单独的请求参数 
- @ApiImplicitParams() 用于方法,包含多个 @ApiImplicitParam

用户模块集成swagger2

  1. 导入swagger2依赖
<!-- 导入swagger依赖 -->
    <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger2</artifactId>
      <version>2.8.0</version>
  </dependency>
  <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger-ui</artifactId>
      <version>2.8.0</version>
  </dependency>
  1. 编写配置集成代码
@Configuration
@EnableSwagger2
public class MySwagger2Config {
 @Bean
 public Docket buildDocket() {
  return new Docket(DocumentationType.SWAGGER_2)
    // 文档信息
    .apiInfo(buildApiInfo()).select()
    // 需要生成文档的包路径
    .apis(RequestHandlerSelectors.basePackage("")).paths(PathSelectors.any()).build();
 }
 /**
  * - 构建文档信息
  * 
  * @return
  */
 private ApiInfo buildApiInfo() {
  return new ApiInfoBuilder().title("用户微服务").description("用户微服务swagger2 UI接入").version("1.0").build();
 }
}
  1. 启动类开启注解@EnableSwagger2
@SpringBootApplication
@EnableEurekaClient
@EnableSwagger2
public class MsUserApplication {
 public static void main(String[] args) {
  SpringApplication.run(MsUserApplication.class, args);
 }
}
  1. 使用swagger2注解为对应控制器声明接口说明
@RestController
@RequestMapping("/user")
@Api(description="用户核心api")
public class UserController {
 @Autowired
 private UserServiceImpl userServiceImpl ;
 @GetMapping()
 @ApiOperation(value="查询所有用户")
 public List<User> findAll() {
  return userServiceImpl.findAll() ;
 }
 @ApiOperation(value="通过用户ID查询用户")
 @GetMapping("/{userId}")
 public User findById(@PathVariable int userId) {
  System.out.println("cluster-2");
  return userServiceImpl.findById(userId) ;
 }
 @PostMapping
 @ApiOperation(value="保存用户")
 public String save(@RequestBody User user) {
  userServiceImpl.save(user);
  return "save 成功" ;
 }
 @PutMapping("/{userId}")
 @ApiOperation(value="更新用户")
 public String update(@PathVariable int userId,@RequestBody User user) {
  userServiceImpl.update(userId, user);
  return "update 成功" ;
 }
 @DeleteMapping("/{userId}")
 @ApiOperation(value="删除用户")
 public String delete(@PathVariable int userId) {
  userServiceImpl.delete(userId);
  return "delete 成功" ;
 }
}
  1. 访问验证
    http://localhost:9001/swagger-ui.html
    部署成功
    在这里插入图片描述

电影微服务集成swagger2

同用户微服务

gateway模块集成swagger2

  1. 导入swagger2依赖(同用户微服务)
  2. 编写配置集成代码
@Component
@Primary
public class MySwaggerResourcesProvider implements SwaggerResourcesProvider {
 private final RouteLocator routeLocator;
 public MySwaggerResourcesProvider(RouteLocator routeLocator) {
  this.routeLocator = routeLocator;
 }
 @Override
 public List<SwaggerResource> get() {
  List<SwaggerResource> resources = new ArrayList<>();
  List<Route> routes = routeLocator.getRoutes();
  routes.forEach(route -> {
   resources.add(swaggerResource(route.getId(), route.getFullPath().replace("**", "v2/api-docs")));
  });
  return resources;
 }
 private SwaggerResource swaggerResource(String name, String location) {
  SwaggerResource swaggerResource = new SwaggerResource();
  swaggerResource.setName(name);
  swaggerResource.setLocation(location);
  swaggerResource.setSwaggerVersion("2.0");
  return swaggerResource;
 }
}
  1. 启动类开启注解(同用户微服务)
  2. 访问验证
    可以通过右上角的切换不同的微服务模块
    在这里插入图片描述
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值