【分布式核心技术】Dubbo技术入门

修改时间:2020年3月8日
作者:pp_x
邮箱:pp_x12138@163.com

Dubbo概述

  • Dubbo是一个分布式系统框架
  • Dubbo 采用全 Spring 配置方式,透明化接入应用,对应用没有任何 API 侵入,只需用 Spring 加载Dubbo 的配置即可,Dubbo 基于 Spring 的 Schema 扩展 进行加载。

如果不想使用 Spring 配置,可以通过 API 的方式 进行调用

什么是分布式系统

  • 分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统
  • 分布式系统(distributed system)是建立在网络之上的软件系统
  • 简单来说:多个(不同职责)人共同来完成一件事
  • 三个臭皮匠赛过诸葛亮,就是分布式系统的真实写照

单一应用架构

  • 当网站流量很小时,只需要一个应用,将所有的功能部署到一起(所有业务都放在一个tomcat里),从而减少部署节点和成本;
  • 此时,用于简化增删改查工作量的数据访问框架 (ORM)是关键;
    在这里插入图片描述

单一应用架构优缺点

  • 优点
    • 小项目开发快 成本低
    • 架构简单
    • 易于测试
    • 易于部署
  • 缺点
    • 大项目模块耦合严重 不易开发 维护 沟通成本高
    • 新增业务困难
    • 核心业务与边缘业务混合在一块,出现问题互相影响

垂直应用架构

  • 当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成几个互不相干的几个应用以提高效率;
  • 大模块按照mvc分层模式,进行拆分成多个互不相关的小模块,并且每个小模块都有独立的服务器
  • 此时,用于加速前端页面开发的web框架(MVC)是关键;因为每个小应用都有独立的页面
    在这里插入图片描述
  • 模型视图控制器(MVC)

垂直应用架构优缺点

  • 优点:
    • 每个应用有独立的页面,便于前端开发
  • 缺点
    • 模块之间不可能完全没有交集,公用模块无法重复利用,开发性的浪费

分布式服务架构

  • 当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的业务,逐渐形成稳健的服务中心,使前端应用能更快速的响应多变的市场需求;
  • 此时,用户提高业务复用及整合的分布式服务框架(RPC)远程调用是关键;
    在这里插入图片描述
  • RPC:独立的应用服务器之间,要依靠RPC(Romote Procedure Call)才能调用
  • 缺点:假如物流服务不忙,其也是100台服务器,商品服务特别忙也是100台服务器,造成资源浪费

流动计算架构

  • 当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐呈现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率
  • 此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键
  • SOA:面向服务架构(Service-Oriented Architecture),简单理解就是“服务治理”,例如:公交车站的调度员
    在这里插入图片描述

Dubbo简介

  • Dubbo是分布式服务框架,是阿里巴巴的开源项目,现交给apache进行维护
  • Dubbo致力于提高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案
  • 简单来说,Dubbo是个服务框架,如果没有分布式的需求,是不需要用的

RPC

  • RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式
  • RPC通信原理
    • 在客户端将对象进行序列化
    • 底层通信框架使用netty(基于tcp协议的socket),将序列化的对象发给服务方提供方
    • 服务提供方通过socket得到数据文件之后,进行反序列化,获得要操作的对象
    • 对象数据操作完毕,将新的对象序列化,再通过服务提供方的socket返回给客户端
    • 客户端获得序列化数据,再反序列化,得到最新的数据对象,至此,完成一次请求
  • PRC两个核心:通信(Socket)和序列化(Serilizable)
    在这里插入图片描述

节点角色

  • Provider: 服务的提供方
  • Consumer :服务的消费方
  • Registry :服务注册与发现的注册中心
  • Monitor :监控服务的统计中心
  • Container :服务运行容器
    在这里插入图片描述

调用关系

  • 服务容器负责启动,加载,运行服务提供者
  • 服务提供者在启动时,向注册中心注册自己提供的服务
  • 服务消费者在启动时,向注册中心订阅自己所需的服务
  • 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
  • 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用;
  • 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心

Dubbo快速入门

注册中心

  • 官方推荐使用zookeeper注册中心;
  • 注册中心负责服务地址的注册与查找,相当于目录服务;
  • 服务提供者和消费者只在启动时与注册中心交互,注册中不转发请求,压力较小;
  • Zookeeper是apache hadoop的子项目,是一个树形的目录服务,支持变更推送,适合作为dubbo的服务注册中心,工业强度较高,可用于生产环境
  • 可以将dubbo比作求职的人和招聘公司,zookeeper是求职网站/人才市场

安装

服务提供方

服务方的pox.xml

  • 采用maven自带的tomcat插件,将端口号设为8001
<packaging>war</packaging>

<!-- 指定编码及版本 -->
<properties>
    <spring.version>5.0.6.RELEASE</spring.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.encoding>UTF-8</maven.compiler.encoding>
    <java.version>1.11</java.version>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <!--dubbo -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>dubbo</artifactId>
        <version>2.5.7</version>
    </dependency>
    <dependency>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
        <version>3.4.6</version>
    </dependency>
    <dependency>
        <groupId>com.github.sgroschupf</groupId>
        <artifactId>zkclient</artifactId>
        <version>0.1</version>
    </dependency>
    <dependency>
        <groupId>javassist</groupId>
        <artifactId>javassist</artifactId>
        <version>3.11.0.GA</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.tomcat.maven </groupId>
            <artifactId>tomcat7-maven-plugin</artifactId>
            <configuration>
                <port>8001</port>
                <path>/</path>
            </configuration>
            <executions>
                <execution>
                    <!-- 打包完成后,运行服务 -->
                    <phase>package</phase>
                    <goals>
                        <goal>run</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

服务提供方接口实现类(接口省略)

@com.alibaba.dubbo.config.annotation.Service
public class HelloServiceImpl implements HelloService {

    public String sayHello(String name) {
        return "Hello " + name;
    }

}

服务提供方配置文件spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://code.alibabatech.com/schema/dubbo
       http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <!-- 1.服务提供方在 zookeeper 中的“别名” -->
    <dubbo:application name="dubbo-server"/>

    <!-- 2.注册中心的地址 -->
    <dubbo:registry address="zookeeper://192.168.227.128:2181"/>

    <!-- 3.扫描类(将什么包下的类作为服务提供类) -->
    <dubbo:annotation package="service.impl"/>

</beans>

服务提供方的web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         id="WebApp_ID" version="3.1">

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring/spring.xml</param-value>
    </context-param>

</web-app>

服务消费方

服务消费方的pom.xml

  • 与服务提供方一致,更改端口号即可

消费方的controller

package controller;

import com.alibaba.dubbo.config.annotation.Reference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import service.HelloService;
@Controller
public class HelloAction {

    // 远程去服务提供方将 service 的实现类注入进来 
    //相当于远程的@AutoWried
    @Reference
    private HelloService helloService;

    @GetMapping("hello")
    @ResponseBody
    public String sayHi(String name){
        return helloService.sayHello(name);
    }

}

消费方的接口

  • 由于controlle需要依赖service,所以需要创建一个同于提供方的service接口,但是不需要实现,dobbo会远程调用提供方的service
public interface HelloService {
    String sayHello(String name);
}

消费方的web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         id="WebApp_ID" version="3.1">

    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring/spring.xml</param-value>
        </init-param>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>

消费方的spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://code.alibabatech.com/schema/dubbo
       http://code.alibabatech.com/schema/dubbo/dubbo.xsd">

    <!-- 1.服务提供方在 zookeeper 中的“别名” -->
    <dubbo:application name="dubbo-consumer"/>

    <!-- 2.注册中心的地址 -->
    <dubbo:registry address="zookeeper://192.168.227.128:2181"/>

    <!-- 3.扫描类(将什么包下的类作为服务消费类) -->
    <dubbo:annotation package="controller"/>

</beans>

启动服务测试

  • 首先启动服务方,再启动消费方。
  • 访问:http://localhost:8002/hello?name=ppx(自己根据访问路径设置)

监控中心

  • 在开发时,需要知道注册中心都注册了哪些服务,以便开发和测试。
  • 可以通过部署一个 web 应用版的管理中心来实现图形化显示注册中心的服务列表

服务管理端

管理端安装

  • 下载解压dubbo-admin-master.ziphttps://github.com/apache/dubbo-admin/tree/master

  • 修改配置文件 dubbo-admin-master\dubbo-admin\src\main\resources\application.properties的端口号和注册中心的地址

      # 管理端的端口号,可以随意改为一个没有被占用的端口号
      server.port=7001
      spring.velocity.cache=false
      spring.velocity.charset=UTF-8
      spring.velocity.layout-url=/templates/default.vm
      spring.messages.fallback-to-system-locale=false
      spring.messages.basename=i18n/message
      spring.root.password=root
      spring.guest.password=guest
      dubbo.registry.address=zookeeper://192.168.227.128:2181
    
  • 返回到项目根目录dubbo-admin-master\dubbo-admin,使用 maven 打包:mvn clean package,生成 target 目录

  • 在 dos 下运行 target 目录中的 jar 文件: java -jar dubbo-admin-0.0.1-SNAPSHOT.jar

  • 此时打开浏览器输入:http://localhost:7001/;第一次访问时,需要登录,帐号密码都是 root

监控统计中心

安装

  • 下载并解压 dubbo-monitor-simple-2.5.3.zip

  • 修改 dubbo-monitor-simple-2.5.3\conf\dubbo.properties

      dubbo.container=log4j,spring,registry,jetty
      dubbo.application.name=simple-monitor
      dubbo.application.owner=
      #dubbo.registry.address=multicast://192.168.186.128:2181
      dubbo.registry.address=zookeeper://192.168.186.128:2181
      #dubbo.registry.address=redis://127.0.0.1:6379
      #dubbo.registry.address=dubbo://127.0.0.1:9090
      dubbo.protocol.port=7070
      dubbo.jetty.port=8080
      dubbo.jetty.directory=${user.home}/monitor
      dubbo.charts.directory=${dubbo.jetty.directory}/charts
      dubbo.statistics.directory=${user.home}/monitor/statistics
      dubbo.log4j.file=logs/dubbo-monitor-simple.log
      dubbo.log4j.level=WARN
    
  • 双击运行 dubbo-monitor-simple-2.5.3\bin\start.bat

  • 分别修改 dubbo-serverdubbo-consumer 的 spring.xml,加入下面标签

<!-- 让监控去注册中心自动找服务 -->
<dubbo:monitor protocol="registry"/>

综合实战

启动时检查

  • 启动时会在注册中心检查依赖的服务是否可用,不可用时会抛出异常。
  • 在消费方编写初始化容器的 main 方法启动(tomcat 启动方式,必须访问一次 action 才能初始化spring)
public class TestCheckException {

    public static void main(String[] args) throws IOException {
        // 初始化spring
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring/spring.xml");
        System.in.read();
    }

}
  • 消费方spring.xml
<!-- 默认是 true:抛异常;false:不抛异常 -->
<dubbo:consumer check="false"/>
  • 系统级别日志,需要配合 log4j 才输出,在 resources 下添加 log4j.properties,内容如下:
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %m%n

log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=dubbo.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %l  %m%n

log4j.rootLogger=error, stdout,file

超时时间

  • 由于网络和服务端的不可靠性,会导致调用过程中出现阻塞状态,为了避免阻塞导致客户端挂起引起资源浪费,必须设置超时时间,规定时间内没有得到响应,会报异常
  • dubbo推荐在提供方尽量多配置消费方的属性:
    • 作服务的提供者,比服务使用方更清楚服务性能参数,如调用的超时时间,合理的重试次数,等等
<!-- 设置超时时间为 2 秒,默认为 1 秒 -->
<dubbo:provider timeout="2000"/>
  • 将提供方线程睡眠三秒进行测试
@Override
public String sayHello(String name) {
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "Hello " + name;
}

  • 超时时间2秒,睡眠时间3秒,超过规定超时时间,报异常

重试次数

  • 当出现失败,自动切换并重试其它服务器,dubbo 重试的默认值是 2 次,可以自行设置
  • 提供方配置
<!-- 设置超时时间为 2 秒,默认为 1 秒;设置重试次数为 3,一共最多执行 4 次 -->
<dubbo:provider timeout="2000" retries="3"/>

重试次数注意点

  • 并不是所有的方法都适合设置重试次数:
    • 幂等方法:适合设置重试(当参数一样,无论执行多少次,结果是一样的;例如,查询,修改)
    • 非幂等方法:不适合设置重试(当参数一样,执行结果不一样;例如,删除,添加)
  • 单独设置某个方法
  • 提供方接口添加 sayNo() 方法并实现
public interface HelloService {
    String sayHello(String name);
    String sayNo();
}
@Service
public class HelloServiceImpl implements HelloService {

    @Override
    public String sayHello(String name) {
        System.out.println("=============sayHello 被调用 1 次===============");
        try {
            // 模拟网络延迟
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "Hello " + name;
    }

    @Override
    public String sayNo() {
        System.out.println("=============sayNo 被调用 1 次===============");
        return "no";
    }

}
  • 消费方添加接口
public interface HelloService {
    String sayHello(String name);
    String sayNo();
}
  • 消费方Controller
@Controller
public class HelloAction {

    // 远程去服务提供方将 service 的实现类注入进来
    // @Reference //  此注解已经在 xml 文件中被 <dubbo:reference> 顶替,所以自动注入即可
    @Autowired
    private HelloService helloService;

    @GetMapping("hello")
    @ResponseBody
    public String sayHi(String name){
        return helloService.sayHello(name);
    }

    @GetMapping("no")
    @ResponseBody
    public String no(){
        return helloService.sayNo();
    }

}
  • 单独方法配置
    • 当配置类reference标签后,@Refernce注解可以改为@AutoWried注解
<dubbo:reference interface="com.renda.service.HelloService" id="helloService">
    <dubbo:method name="sayHello" retries="3"/>
    <dubbo:method name="sayNo" retries="0"/>
</dubbo:reference>

多版本

  • 一个接口,多个(版本的)实现类,可以使用定义版本的方式引入
  • 为 HelloService 接口定义两个实现类,提供者修改配置
<dubbo:service interface="com.renda.service.HelloService" class="com.renda.service.impl.HelloService01Impl"
               version="1.0.0"/>
<dubbo:service interface="com.renda.service.HelloService" class="com.renda.service.impl.HelloService02Impl"
               version="2.0.0"/>
  • 消费者就可以根据 version 的版本,选择具体的服务版本
<dubbo:reference interface="com.renda.service.HelloService" id="helloService" version="1.0.0">
    <dubbo:method name="sayHello" retries="3"/>
    <dubbo:method name="sayNo" retries="0"/>
</dubbo:reference>
  • 当消费者的版本修改为 version="*",那么就会随机调用服务提供者的版本

本地存根

  • 远程服务后,客户端通常只剩下接口,而接口的实现都在服务端,这样会大大加大服务端的运行压力,有时想在客户端也执行一些逻辑,如做 ThreadLocal 缓存,提前验证参数,调用失败后伪造容错数据等等,此时就需要在 API 中带上 Stub,客户端生成 Proxy 实例,会把 Proxy 通过构造函数传给 Stub,然后把 Stub 暴露给用户,Stub 可以决定要不要去调 Proxy
  • 先在消费者处理一些业务逻辑,再调用提供者的过程,就是“本地存根”
  • 代码实现在消费者,创建一个 HelloServiceStub 类并且实现 HelloService 接口
  • 必须使用构造方法形式注入对象
public class HelloServiceStub implements HelloService {

    // helloService 的代理对象
    private final HelloService helloService;

    /**
     * 本地存根必须以构造方法的形式注入
     */
    public HelloServiceStub(HelloService helloService) {
        this.helloService = helloService;
    }

    @Override
    public String sayHello(String name) {
        System.out.println("HelloServiceStub:本地存根");
        if(!StringUtils.isEmpty(name)){
            return helloService.sayHello(name);
        }
        return "Name is Empty";
    }

    @Override
    public String sayNo() {
        return helloService.sayNo();
    }

}
  • 修改消费方配置
<dubbo:reference interface="com.renda.service.HelloService" id="helloService" version="2.0.0"
                 stub="com.renda.service.stub.HelloServiceStub">
    <dubbo:method name="sayHello" retries="3"/>
    <dubbo:method name="sayNo" retries="0"/>
</dubbo:reference>

负载均衡策略

  • 负载均衡 - Load Balance,其实就是将请求分摊到多个操作单元上进行执行,从而共同完成工作任务
  • Dubbo 提供了** 4 种负载均衡实现**,分别是基于权重随机算法的 RandomLoadBalance(默认)、基于最少活跃调用数算法的 LeastActiveLoadBalance基于 hash 一致性的ConsistentHashLoadBalance,以及基于加权轮询算法的 RoundRobinLoadBalance

在这里插入图片描述

高可用

zookeeper宕机

ZooKeeper 注册中心宕机,还可以消费 dubbo 暴露的服务

  • 监控中心宕掉不影响使用,只是丢失部分采样数据
  • 数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
  • 注册中心对等集群,任意一台宕掉后,将自动切换到另一台
  • 注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
  • 服务提供者无状态,任意一台宕掉后,不影响使用
  • 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复

服务降级

  • 类似双十一秒杀,0点是订单服务器负载严重,而此时大家对广告服务、评价服务等其他服务的要求会很少,此时可以将这类服务停止或简单化,释放资源提供给订单服务运行。
  • 服务降级,就是根据实际的情况和流量,对一些服务有策略的停止或换种简单的方式处理,从而释放服务器的资源来保证核心业务的正常运行。

为什么要服务降级

  • 防止发生蝴蝶效应
  • 如果一个请求发生超时,一直等待服务器响应,在高并发的情况下,很多请求都是这样等待服务器响应,知道资源耗尽产生宕机,而一个服务宕机后,其他调用该服务的服务也会出现资源耗尽而宕机的情况,这样下去就会使整个分布式服务都瘫痪,这就是蝴蝶效应(雪崩效应)

服务降级实现方式

  • 屏蔽mock=force:return+null表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用,用来屏蔽不重要服务不可用时对调用方的影响
  • 容错mock=fail:return+null 表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值