SpringCloud

SpringCloud

一、SpringCloud 的五大组件

(需要牢牢记住他们,现在混个眼熟,下面会详细介绍。)

自学参考文档:

  • SpringCloud 官方文档(汉化版):https://springcloud.cc/spring-cloud-dalston.html
  • SpringCloud中国社区:http://springcloud.cn/
  • SpringCloud中文网:https://springcloud.cc

自学参考文章:

  • 一文详解微服务架构:https://www.cnblogs.com/skabyy/p/11396571.html

自学参考知乎各个大神们的回答:

  • 微服务架构是什么?:https://www.zhihu.com/question/65502802

自学参考视频:

  • kuangstudy:https://www.kuangstudy.com/course/play/1321005531116863490

二、什么是微服务

Spring官网:https://spring.io/

微服务(Microservice Architecture) 是近几年流行的一种架构思想,关于它的概念很难一言以蔽之。究竟什么是微服务呢?我们在此引用ThoughtWorks 公司的首席科学家 Martin Fowler 于2014年提出的一段话:

原文:https://martinfowler.com/articles/microservices.html

中文:https://www.cnblogs.com/liuning8023/p/4493156.html

就目前而言,对于微服务,业界没有一个统一的标准定义。通常而言,微服务架构是一种架构模式,或者说是一种架构风格,它提倡将单一的应用程序划分为一组小的服务,每个服务运行在其独特的自己的进程内,服务之间互相协调,互相配置,为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通,每个服务都围绕着具体的业务进行构建,并且能够被独立的部署到生产环境中。另外,应尽量避免统一的,集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择适合的语言,工具对其进行构建,可以有一个非常轻量级的集中管理来协调这些服务,可以用不同的语言来编写服务,也可以使用不同的数据库。 (牢牢记住上面这段概念,几乎每句话都有微服务的关键点)

三、传统开发模式 VS 微服务

  • 传统的web开发方式

一般被称为Monolithic(单体式开发)。所有的功能打包在一个 WAR包里,基本没有外部依赖(除了容器),部署在一个JEE容器(Tomcat,JBoss,WebLogic)里,包含了 DO/DAO,Service,UI等所有逻辑。

优点:
集中式管理,开发简单,功能都在本地,没有分布式的管理和调用。

缺点:
1.效率低,开发都在同一个项目改代码,互相等待,冲突不断;
2.稳定性差,一个微小的问题,可能导致整个程序挂掉;
3.维护性难,代码高耦合,内部关系难以摸清楚;
4.扩展性差,无法满足高并发下的业务需求;

  • 随着业务的发展,移动端兴起

这一阶段,架构设计存在着很多不合理的地方:
1.网站和移动端存在着很多相同业务逻辑的重复代码;
2.数据库被多个应用依赖,无法重构和优化;
3.数据有时候通过数据库共享,有时候通过接口调用传输,接口调用关系杂乱;
4.单个应用为了给其他应用提供接口,设计得越来越复杂,包含本不属于它得逻辑;
5.应用之间界限模糊,功能归属混乱,出现问题后各部门职责很难划分,出现分歧或争端;
6.所有的应用都在一个数据库上操作,数据库出现性能瓶颈;
7.管理后台保障级别比较低,添加新的功能可能影响到其他应用;
8.开发、部署、维护、升级愈发困难;

  • 下一阶段,除去大量的冗余代码

在这一阶段,服务已经被拆分开了,但是数据库依然是共用的。会出现一些问题:
1.数据库性能瓶颈,而且存在一定的风险;
2.数据库表结构可能被多个服务依赖,维护困难;

  • 提高系统的实时性,再次升级架构(微服务)

此时,拆分后的各个服务可以采用异构的技术。比如,数据分析服务可以使用数据仓库作为持久化层,以便于高效地做一些统计计算;促销服务访问比较频繁,因此可以加入缓存机制。

微服务,它是具体解决某一个问题/提供落地对应服务的一个服务应用,狭义的看,可以看作是IDEA中的一个个微服务工程,或者Moudel。IDEA 工具里面使用Maven开发的一个个独立的小Moudel,它具体是使用SpringBoot开发的一个小模块,专业的事情交给专业的模块来做,一个模块就做着一件事情。强调的是一个个的个体,每个个体完成一个具体的任务或者功能。

微服务的优点:
1.单一职责原则;
2.每个服务足够内聚,足够小,代码容易理解;
3.开发效率高,一个服务可能就是专一的只干一件事;
4.微服务能够被小团队单独开发,这个团队只需2-5个开发人员组成;
5.松耦合,无论是在开发阶段或部署阶段都是独立的;
6.可以使用不同的语言开发;
7.易于和第三方集成,微服务允许容易且灵活的方式集成自动部署,通过持续集成工具,如jenkins,Hudson,bamboo;
8.每个微服务都有自己的存储能力,可以有自己的数据库,也可以有统一的数据库;

微服务的缺点:
1.开发人员要处理分布式系统的复杂性;
2.多服务运维难度,随着服务的增加,运维的压力也在增大;
3.各个服务间的通信成本问题;
4.整个应用分散成多个服务,定位故障相对困难;
5.一个服务故障可能产生雪崩效用,导致整个系统故障;

整体解决思路如下:

四、SpringCloud入门

官网:http://projects.spring.io/spring-cloud/

SpringCloud没有采用数字编号的方式命名版本号,而是采用了伦敦地铁站的名称,同时根据字母表的顺序来对应版本时间顺序,比如最早的Realse版本:Angel,第二个Realse版本:Brixton,然后是Camden、Dalston、Edgware,目前最新的是Hoxton SR4 CURRENT GA通用稳定版。

  • 微服务技术栈有那些?
微服务技术条目落地技术
服务开发SpringBoot、Spring、SpringMVC等
服务配置和管理Netfix公司的Archaius、阿里的Diamond等
服务注册与发现Eureka、Consul、Zookeeper等
服务调用Rest、PRC、gRPC
服务熔断器Hystrix、Envoy等
负载均衡Ribbon、Nginx等
服务接口调用(客户端调用服务的简化工具Fegin等
消息队列Kafka、RabbitMQ、ActiveMQ等
服务配置中心管理SpringCloudConfig、Chef等
服务路由(API网关)Zuul等
服务监控Zabbix、Nagios、Metrics、Specatator等
全链路追踪Zipkin、Brave、Dapper等
数据流操作开发包SpringCloud Stream(封装与Redis,Rabbit,Kafka等发送接收消息)
时间消息总站SpringCloud Bus
服务部署Docker、OpenStack、Kubernetes等
  • 各微服务框架对比
功能点/服务框架Netflix/SpringCloudMotangRPCThritDubbo/DubboX
功能定位完整的微服务框架RPC框架,但整合了ZK或Consul,实现集群环境的基本服务注册发现RPC框架RPC框架服务框架
支持Rest是,Ribbon支持多种可拔插的序列号选择
支持RPC是(Hession2)
支持多语言是(Rest形式)
负载均衡是(服务端zuul+客户端Ribbon),zuul-服务,动态路由,云端负载均衡Eureka(针对中间层服务器)是(客户端)是(客户端)
配置服务Netfix Archaius,Spring Cloud Config Server 集中配置是(Zookeeper提供)
服务调用链监控是(zuul),zuul提供边缘服务,API网关
高可用/容错是(服务端Hystrix+客户端Ribbon)是(客户端)是(客户端)
典型应用案例NetflixSinaGoogleFacebook
社区活跃程度一般一般2017年后重新开始维护,之前中断了5年
学习难度中等
文档丰富程度一般一般一般
其他Spring Cloud Bus为我们的应用程序带来了更多管理端点支持降级Netflix内部在开发集成gRPCIDL定义实践的公司比较多

  • Rest 搭建学习环境

1.创建父工程 springcloud (使用 pom 打包方式)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zhou</groupId>
    <artifactId>springcloud</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>springcloud-consumer-dept-80</module>
        <module>springcloud-provider-dept-8001</module>
        <module>springcloud-api</module>
    </modules>
    <!--打包方式 pom -->
    <packaging>pom</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <junit.version>4.12</junit.version>
        <log4j.version>1.2.17</log4j.version>
        <lombok.version>1.16.18</lombok.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>0.2.0.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--SpringCloud的依赖-->
            <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-dependencies -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.SR1</version>
                <type>pom</type>
                <scope>runtime</scope>
            </dependency>
            <!--SpringBoot-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.1.4.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--数据库-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>5.1.47</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.2.5</version>
            </dependency>
            <!--SpringBoot 启动器-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>1.3.2</version>
            </dependency>
            <!--日志测试-->
            <dependency>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-core</artifactId>
                <version>1.2.3</version>
            </dependency>
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>${junit.version}</version>
            </dependency>
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>${log4j.version}</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
        <!--Maven 资源过滤问题-->
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.*</include>
                </includes>
                <filtering>false</filtering>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.*</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>
</project>

注意:父工程为 springcloud,其下有多个子 module

2.创建子模块 springcloud-api,它只负责接管 pojo

pom 依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud</artifactId>
        <groupId>com.zhou</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-api</artifactId>

    <dependencies>
        <!--当前的module自己需要的依赖,如果父类中已经配置了版本,这里就不需要写了-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

3.在SQLyog中创建数据库 db01

idea连接此数据库并创建一些字段和数据

insert into dept (dname, db_source) VALUES ('开发部',DATABASE());
insert into dept (dname, db_source) VALUES ('设计部',DATABASE());
insert into dept (dname, db_source) VALUES ('人事部',DATABASE());
insert into dept (dname, db_source) VALUES ('运营部',DATABASE());
insert into dept (dname, db_source) VALUES ('企划部',DATABASE());
insert into dept (dname, db_source) VALUES ('编辑部',DATABASE());

4.创建实体类

package com.zhou.springcloud.pojo;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

import java.io.Serializable;

@Data
@NoArgsConstructor
@Accessors(chain = true) //链式编程,默认 boolean chain() default false;
public class Dept implements Serializable { //Dept 实体类

    private Long deptno;//主键
    private String dname;
    //这个数据存在那个数据库的字段,微服务,一个服务对应一个数据库,同一个信息可能存在不同的数据库
    private String db_source;

    public Dept(String dname) {
        this.dname = dname;
    }
}

5.创建子模块 springcloud-provider-dept-8001 (服务的提供者)

子模块 springcloud-provider-dept-8001 的整体项目结构如下:

pom 依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud</artifactId>
        <groupId>com.zhou</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-provider-dept-8001</artifactId>

    <dependencies>
        <!--我们需要拿到实体类,所以要配置 api module-->
        <dependency>
            <groupId>com.zhou</groupId>
            <artifactId>springcloud-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--junit-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!--test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--jetty-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jetty</artifactId>
        </dependency>
        <!--热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
    </dependencies>
</project>

application.yaml

server:
  port: 8001

#mybatis 配置
mybatis:
  type-aliases-package: com.zhou.springcloud.pojo
  config-location: classpath:mybatis/mybatis-config.xml
  mapper-locations: classpath:mybatis/mapper/*.xml

#speing 配置
spring:
  application:
    name: springcloud-provider-dept
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/db01?useUnicode=true&characterEncoding=utf-8
    username: root
    password: root

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!--开启二级缓存-->
        <setting name="cacheEnabled" value="true"/>
    </settings>
</configuration>

DeptMapper 接口

package com.zhou.springcloud.mapper;

import com.zhou.springcloud.pojo.Dept;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Mapper
@Repository
public interface DeptMapper {
    boolean addDept(Dept dept);
    Dept queryById(@Param("id") Long id);
    List<Dept> queryAll();
}

DeptMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zhou.springcloud.mapper.DeptMapper">
    <insert id="addDept" parameterType="Dept">
        insert into dept (dname, db_source)
        values (#{dname},DATABASE());
    </insert>
    <select id="queryById" parameterType="Long" resultType="Dept">
        select * from dept where deptno=#{id};
    </select>
    <select id="queryAll" resultType="Dept">
        select * from dept
    </select>
</mapper>

DeptService 接口

package com.zhou.springcloud.service;
import com.zhou.springcloud.pojo.Dept;
import java.util.List;

public interface DeptService {
    boolean addDept(Dept dept);
    Dept queryById(Long id);
    List<Dept> queryAll();
}

DeptServiceImpl.java

package com.zhou.springcloud.service;

import com.zhou.springcloud.mapper.DeptMapper;
import com.zhou.springcloud.pojo.Dept;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
@Service
public class DeptServiceImpl implements DeptService{
    @Autowired
    private DeptMapper deptMapper;
    @Override
    public boolean addDept(Dept dept) {
        return deptMapper.addDept(dept);
    }

    @Override
    public Dept queryById(Long id) {
        return deptMapper.queryById(id);
    }

    @Override
    public List<Dept> queryAll() {
        return deptMapper.queryAll();
    }
}

DeptController.java

package com.zhou.springcloud.controller;

import com.zhou.springcloud.pojo.Dept;
import com.zhou.springcloud.service.DeptService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

//提供Restful服务
@RestController
public class DeptController {
    @Autowired
    private DeptService deptService;

    @GetMapping("/dept/add")//方便此时的测试,这里用了Get,没有用Post
    public boolean addDept(Dept dept) {
        return deptService.addDept(dept);
    }

    @GetMapping("/dept/get/{id}")
    public Dept getDept(@PathVariable("id") Long id){
        return deptService.queryById(id);
    }

    @GetMapping("/dept/list")
    public List<Dept> queryAll(){
        return deptService.queryAll();
    }
}

6.启动类

package com.zhou.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

//启动类
@SpringBootApplication
public class DeptProvider_8001 {
    public static void main(String[] args) {
        SpringApplication.run(DeptProvider_8001.class,args);
    }
}

7.Run 测试

访问:http://localhost:8001/dept/list (测试成功!)

访问:http://localhost:8001/dept/get/1 (测试成功!)

访问:http://localhost:8001/dept/add?dname=地狱部 (测试成功!)

查询数据库


1.创建子模块 springcloud-consumer-dept-80 (服务的消费者)

pom 依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud</artifactId>
        <groupId>com.zhou</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-consumer-dept-80</artifactId>

    <dependencies>
        <!--不需要连接数据库,需要实体类+web-->
        <dependency>
            <groupId>com.zhou</groupId>
            <artifactId>springcloud-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
</dependencies>
</project>

application.yml

server:
  port: 80

ConfigBean.java

package com.zhou.springcloud.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration //@Configuration 相当于 spring中 applicationContext.xml
public class ConfigBean {
    @Bean
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
}

RestTemplate 部分源码:

DeptConsumerController.java

package com.zhou.springcloud.controller;

import com.zhou.springcloud.pojo.Dept;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@Controller
public class DeptConsumerController {
    //理解:消费者,不应该有 service 层
    //RestTemplate.... 供我们直接调用就可以了,注册到Spring中
    @Autowired
    private RestTemplate restTemplate;

    //http://localhost:8081/dept/add?dname=地狱部
    private static final String REST_URL_PREFIX="http://localhost:8081";

    @RequestMapping("/consumer/dept/add")
    @ResponseBody
    public boolean add(Dept dept){
        return restTemplate.postForObject(REST_URL_PREFIX + "/dept/add", dept,Boolean.class);
    }

    @RequestMapping("/consumer/dept/get/{id}")
    @ResponseBody
    public Dept get(@PathVariable("id") Long id){
        return restTemplate.getForObject(REST_URL_PREFIX + "/dept/get/" + id, Dept.class);
    }

    @RequestMapping("/consumer/dept/list")
    @ResponseBody
    public List<Dept> list(){
        return restTemplate.getForObject(REST_URL_PREFIX + "/dept/list", List.class);
    }
}

DeptConsumer_80.java 启动类

package com.zhou.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

2.修改:springcloud-provider-dept-8001 它的 DeptController.java类的addDept()方法

(否则,在下面测试中,使用浏览器url传参的方式来插入数据时,数据库中显示dname的值为null)

    @PostMapping("dept/add")
    public boolean addDept(@RequestBody Dept dept){
        return deptService.addDept(dept);
    }

3.启动

首先,启动服务的提供者 springcloud-provider-dept-8001 的启动类 DeptProvider_8001
其次,启动服务的消费者 springcloud-consumer-dept-80 的启动类 DeptConsumer_80

4.Run 测试

访问:http://localhost/consumer/dept/list

访问:http://localhost/consumer/dept/get/5

访问:http://localhost/consumer/dept/add?dname=架构师3

查看数据库

五、Eureka 服务注册中心

  • Eureka 定义

Eureka 是Netflix的一个子模块,也是核心模块之一。Eureka是基于REST的服务,用于定位服务,以实现云端中间件层服务发现和故障转移,服务注册与发现对于微服务来说是非常重要的,有了服务注册与发现,只需要使用服务的标识符,就可以访问到服务,而不需要修改服务调用的配置文件了,功能类似于Dubbo的注册中心,比如Zookeeper。

在云中,应用程序不能总是知道其他服务的确切位置。一个服务注册中心,比如Netflix Eureka,或者一个sidecar解决方案,比如HashiCorp Consul,都会有所帮助。springcloud为流行的注册中心提供DiscoveryClient实现,比如Eureka、Consul、Zookeeper,甚至Kubernetes的内置系统。还有一个springcloud负载均衡器可以帮助您在服务实例之间小心地分配负载。

官方介绍:https://spring.io/projects/spring-cloud-netflix

springcloudnetflix通过自动配置并绑定到Spring环境和其他Spring编程模型习惯用法,为Spring启动应用程序提供Netflix操作系统集成。通过一些简单的注释,您可以快速启用和配置应用程序中的常见模式,并使用经过测试的Netflix组件构建大型分布式系统。提供的模式包括服务发现(Eureka)、断路器(Hystrix)、智能路由(Zuul)和客户端负载平衡(Ribbon)

  • Dubbo 和 SpringCloud对比

最大区别:Spring Cloud 抛弃了Dubbo的RPC通信,采用的是基于HTTP的REST方式。
二者解决的问题域不一样:Dubbo的定位是一款RPC框架,而SpringCloud的目标是微服务架构下的一站式解决方案。

DubboSpringCloud
服务注册中心ZookeeperSpring Cloud Netfilx Eureka
服务调用方式RPCREST API
服务监控Dubbo-monitorSpring Boot Admin
断路器不完善Spring Cloud Netfilx Hystrix
服务网关Spring Cloud Netfilx Zuul
分布式配置Spring Cloud Config
服务跟踪Spring Cloud Sleuth
消息总栈Spring Cloud Bus
数据流Spring Cloud Stream
批量任务Spring Cloud Task

严格来说,这两种方式各有优劣。虽然从一定程度来说,SpringCloud牺牲了服务调用的性能,但也避免了上面提到的原生RPC带来的问题。而且REST相比RPC更为灵活,服务提供方和调用方的依赖只依靠一纸契约,不存在代码级别的强依赖,这个优点在当下强调快速演化的微服务环境下,显得更加合适。

  • Eureka基本的架构

1.Springcloud 封装了Netflix公司开发的Eureka模块来实现服务注册与发现 (对比Zookeeper).
2.Eureka采用了C-S的架构设计,EurekaServer作为服务注册功能的服务器,他是服务注册中心.
3.系统中的其他微服务,使用Eureka的客户端连接到EurekaServer并维持心跳连接。(方便监控系统中各个微服务是否正常运行)

  • 与Dubbo架构对比

  • 构建 Eureka 代码示例

1.创建子模块 springcloud-eureka-7001

pom 依赖

<artifactId>springcloud-eureka-7001</artifactId>
	<!--导入依赖-->
<dependencies>
	<!--spring-cloud-starter-netflix-eureka-server 依赖-->
	<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-eureka-server -->
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
		<version>2.2.7.RELEASE</version>
	</dependency>
	<!--热部署-->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-devtools</artifactId>
	</dependency>
</dependencies>

application.yaml

server:
  port: 7001

# Eureka 部署
eureka:
  instance:
    # Eureka服务端的实例名字
    hostname: localhost
  client:
    # 表示是否向 Eureka 注册中心注册自己(这个模块本事是服务器,所以不需要)
    register-with-eureka: false
    # fetch-registry 如果为 false,则表示自己为注册中心,客户端的为 true
    fetch-registry: false
    # Eureka 监控页面
    service-url:
      #public static final String DEFAULT_URL = "http://localhost:8761/eureka/";
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

启动类 EurekaServer_7001.java

package com.zhou.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

/**
 * @Auther: zhouzhou
 * @Description: 启动之后,访问 http://127.0.0.1:7001/
 */
@SpringBootApplication
@EnableEurekaServer //服务端的启动类,可以接受别人注册进来
public class EurekaServer_7001 {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServer_7001.class,args);
    }
}

Run 测试

报错信息:Caused by: java.lang.ClassNotFoundException: org.springframework.boot.context.properties.ConfigurationPropertiesBean

image

观察报错信息,得出有可能是版本冲突问题,解决办法是将 spring-cloud-starter-netflix-eureka-server 依赖的版本降为2.1.4.RELEASE

【注意】:报错原因是版本问题,可以选择到官网查看版本是否一致,比如 SPRINGCLOUD的版本,我的父依赖用的是GREENWICH.SR1
你如果用了HOXTON.SR10甚至更新的,请自行查找对应的版本依赖。

继续测试,Run 成功!

image

访问:http://localhost:7001/

  • Eureka 服务注册 信息配置以及自我保护机制

配置 Eureka-client

1.在上面创建的子模块 springlouc-provider-dept-8001 的 pom 中添加依赖

<!--Eureka:spring-cloud-starter-netflix-eureka-client 依赖-->
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-eureka-client -->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
	<version>2.1.4.RELEASE</version>
</dependency>

2.application.yaml 中添加设置

# Eureka配置:配置注册中心地址
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka/

3.启动类中使用 @EnableEurekaClient注解

package com.zhou.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

//启动类
@SpringBootApplication
@EnableEurekaClient
public class DeptProvider_8001 {
    public static void main(String[] args) {
        SpringApplication.run(DeptProvider_8001.class,args);
    }
}

4.先启动7001服务端,再启动8001客户端进行测试,访问监控页:http://localhost:7001/ 产看结果如图,成功

5.修改 Eureka 上的默认描述信息

# Eureka配置:配置注册中心地址
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka/
  instance:
    instance-id: springcloud-provider-dept8001 # 修改eureka上的默认描述信息
    prefer-ip-address: true

查看:

6.配置关于服务加载的监控信息(springcloud-provider-dept-8001)

没配置之前,访问:springcloud-provider-dept8001

跳出的页面如下:

配置服务加载的监控信息步骤如下:

pom.xml中添加依赖

<!--actuator完善监控信息-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

application.yaml 中添加配置

# info配置
info:
  # 项目名称
  app.name: zhouzhou-springcloud
  # 公司名称
  company.name: blog.zhouzhou.com

7.Run 再刷新页面,继续访问:springcloud-provider-dept8001,跳出页面如下

  • EureKa自我保护机制

默认情况下,如果Eureka Server在90秒内没有接收到某个微服务实例的心跳,会注销该实例。但是在微服务架构下服务之间通常都是跨进程调用,网络通信往往面临很多问题,比如微服务状态正常,网络分区故障,导致此实例被注销。

固定时间内大量实例被注销,可能会严重威胁某个微服务架构的可用性,为了解决这个问题,Eureka开发了自我保护机制。

Eureka Server在运行期间会去统计心跳失败比例在15分钟之内是否低于85%,如果低于85%,Eureka Server即进入自我保护机制。

Eureka Server触发自我保护机制后,页面会出现提示:

Eureka Server进入自我保护机制,会出现以下几种情况:
(1)Eureka不再从注册列表中移除因为长时间没收到心跳而应该过期的服务;
(2)Eureka仍然能够接受新服务的注册和查询,但是不会被同步到其它节点上(即保证当前节点依然可用);
(3)当网络稳定后,当前实例新的注册信息会被同步到其它节点上;

Eureka自我保护机制是为了防止误杀服务而提供的一种机制。当个别客户端出现心跳失联时,则认为是客户端的问题,剔除客户端;当Eureka 捕获到大量的心跳失败时,则认为可能是网络问题,进入自我保护机制;当客户端心跳恢复时,Eureka会自动退出自我保护机制。

如果在保护期内刚好这个服务提供者非正常下线了,此时服务消费者就会拿到一无效的服务实例,则会调用失败。对于这个问题需要服务消费者要有一些容错机制,比如重试,断路器等。

  • 注册进来的微服务,获取其中的一些信息(团队开发)

1.查看 EurekaDiscoveryClient 源码:

2.观察 DiscoveryClient 源码:

3.在 springcloud-provider-dept-8001 的 DeptController.java中添加 discovery() 方法

//DiscoveryClient 可以用来获取一些配置的信息,得到具体的微服务
    @Autowired
    private DiscoveryClient discoveryClient;

    /**
     * 获取一些注册进来的微服务的信息
     * @return
     */
    @GetMapping("dept/discovery")
    public Object discovery(){
        //获取微服务列表清单
        System.out.println("getServices()=>"+discoveryClient.getServices());
        System.out.println("description()=>"+discoveryClient.description());

        List<ServiceInstance> instances = discoveryClient.getInstances("SPRINGCLOUD-PROVIDER-DEPT");
        for (ServiceInstance instance : instances) {
            System.out.println(
                    instance.getHost()+"\t"+ //主机名称
                    instance.getPort()+"\t"+ //端口号
                    instance.getUri()+"\t"+ //uri
                    instance.getInstanceId() //服务id
            );
        }
        return  this.discoveryClient;
    }

4.上面 discoveryClient.getInstances()的参数 —> SPRINGCLOUD-PROVIDER-DEPT

5.主启动类中加入 @EnableDiscoveryClient 注解

package com.zhou.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

//启动类
@SpringBootApplication
//@EnableEurekaClient 开启Eureka客户端注解,在服务启动后自动向注册中心注册服务
@EnableEurekaClient
//@EnableDiscoveryClient 开启服务发现客户端的注解,可以用来获取一些配置的信息,得到具体的微服务
@EnableDiscoveryClient
public class DeptProvider_8001 {
    public static void main(String[] args) {
        SpringApplication.run(DeptProvider_8001.class,args);
    }
}

6.Run 测试
先启动 springcloud-eureka-7001 中的住启动类 EurekaServer_7001,
再启动 springcloud-provider-dept-8001 中的主启动类 DeptProvider_8001,

访问:http://localhost:7001/ 一切正常

继续访问:http://localhost:8001/dept/discovery

springcloud-provider-dept-8001 控制台输出:

image


  • Eureka:集群环境配置

整体结构如下:

1.新建 子模块springcloud-eureka-7002 和 springcloud-eureka-7003

2.添加 pom 依赖 (与springcloud-eureka-7001相同)

3.application.yml配置 (与springcloud-eureka-7001相同)
(端口号用各自的 7001、7002和7003)

4.主启动类 (与springcloud-eureka-7001相同)

5.集群成员相互关联
配置一些自定义本机名字,在C:\Windows\System32\drivers\etc找到本机hosts文件,在hosts文件最后加上,要访问的本机名称(默认是localhost)

127.0.0.1     eureka7001.com
127.0.0.1     eureka7002.com
127.0.0.1     eureka7003.com

**【注意】:**修改hosts文件后一定要保存,如果遇到 修改hosts文件无权限的问题,参考下图配置:

6.修改 各自的 application.yml 的配置

(1)设置各自的 服务端的实例名字(hostname)
(2)设置各自的 集群(关联)

springcloud-eureka-7001 的 application.yaml

server:
  port: 7001

# Eureka 部署
eureka:
  instance:
    # Eureka服务端的实例名字
    hostname: eureka7001.com
  client:
    # 表示是否向 Eureka 注册中心注册自己(这个模块本事是服务器,所以不需要)
    register-with-eureka: false
    # fetch-registry 如果为 false,则表示自己为注册中心,客户端的为 true
    fetch-registry: false
    # Eureka 监控页面
    service-url:
      # public static final String DEFAULT_URL = "http://localhost:8761/eureka/";
      # 单机:defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
      # 集群(关联):7001关联 7002、7003
      defaultZone: http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/

springcloud-eureka-7002 的 application.yaml

server:
  port: 7002

eureka:
  instance:
    # Eureka服务端的实例名字
    hostname: eureka7002.com
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      # 集群(关联):7002关联 7001、7003
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7003.com:7003/eureka/

springcloud-eureka-7003 的 application.yaml

server:
  port: 7003

eureka:
  instance:
    # Eureka服务端的实例名字
    hostname: eureka7003.com
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      # 集群(关联):7003关联 7001、7002
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/

7.通过 springcloud-provider-dept-8001 的yaml配置文件,修改 Eureka 的配置:配置服务注册中心地址

# Eureka配置:配置注册中心地址
eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
  instance:
    instance-id: springcloud-provider-dept8001 # 修改eureka上的默认描述信息
    prefer-ip-address: true

8.模拟集群搭建完毕。(可以把一个项目挂载到三个服务器上了)

测试,访问:http://localhost:7001/

测试,访问:http://localhost:7002/

测试,访问:http://localhost:7003/


  • Eureka与Zookeeper 的对比

1.CAP原则

  • RDBMS (MySQL\Oracle\sqlServer) ===> ACID
  • NoSQL (Redis\MongoDB) ===> CAP

2.ACID是什么

  • A (Atomicity) 原子性
  • C (Consistency) 一致性
  • I (Isolation) 隔离性
  • D (Durability) 持久性

3.CAP是什么

  • C (Consistency) 一致性
  • A (Availability) 可用性
  • P (Partition tolerance) 分区容错性

CAP的三进二:CA、AP、CP

4.CAP理论的核心
一个分布式系统不可能同时很好的满足一致性,可用性和分区容错性这三个需求
根据CAP原理,将NoSQL数据库分成了满足CA原则,满足CP原则和满足AP原则三大类

  • CA:单点集群,满足一致性,可用性的系统,通常可扩展性较差
  • CP:满足一致性,分区容错的系统,通常性能不是特别高
  • AP:满足可用性,分区容错的系统,通常可能对一致性要求低一些

5.作为分布式服务注册中心,Eureka比Zookeeper好在哪里?

著名的CAP理论指出,一个分布式系统不可能同时满足C (一致性) 、A (可用性) 、P (容错性),由于分区容错性P再分布式系统中是必须要保证的,因此我们只能再A和C之间进行权衡。

Zookeeper 保证的是 CP —> 满足一致性,分区容错的系统,通常性能不是特别高
Eureka 保证的是 AP —> 满足可用性,分区容错的系统,通常可能对一致性要求低一些

  • Zookeeper保证的是CP

当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接收服务直接down掉不可用。也就是说,服务注册功能对可用性的要求要高于一致性。但zookeeper会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举leader的时间太长,30-120s,且选举期间整个zookeeper集群是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因为网络问题使得zookeeper集群失去master节点是较大概率发生的事件,虽然服务最终能够恢复,但是,漫长的选举时间导致注册长期不可用,是不可容忍的。

  • Eureka保证的是AP

Eureka看明白了这一点,因此在设计时就优先保证可用性。Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册时,如果发现连接失败,则会自动切换至其他节点,只要有一台Eureka还在,就能保住注册服务的可用性,只不过查到的信息可能不是最新的,除此之外,Eureka还有之中自我保护机制,如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况:

(1)Eureka不在从注册列表中移除因为长时间没收到心跳而应该过期的服务
(2)Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其他节点上 (即保证当前节点依然可用)
(3)当网络稳定时,当前实例新的注册信息会被同步到其他节点中

因此,Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像zookeeper那样使整个注册服务瘫痪。

六、Ribbon:负载均衡(基于客户端)

Spring Cloud Ribbon 是基于Netflix Ribbon 实现的一套客户端负载均衡的工具。

Ribbon 是 Netflix 发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将 Netflix 的中间层服务连接在一起。Ribbon 的客户端组件提供一系列完整的配置项,如:连接超时、重试等。

  • LB,即负载均衡 (LoadBalancer) ,在微服务或分布式集群中经常用的一种应用。
  • 负载均衡简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA (高用)。
  • 常见的负载均衡软件有 Nginx、Lvs 等等。
  • Dubbo、SpringCloud 中均给我们提供了负载均衡,SpringCloud 的负载均衡算法可以自定义。

负载均衡简单分类:

(1)集中式LB
即在服务的提供方和消费方之间使用独立的LB设施,如Nginx(反向代理服务器),由该设施负责把访问请求通过某种策略转发至服务的提供方!
(2) 进程式 LB
将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选出一个合适的服务器。
Ribbon 就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。

  • 集成 Ribbon

向子模块 springcloud-consumer-dept-80 中的 pom 文件中添加Ribbon和Eureka依赖

<!--Ribbon-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-ribbon</artifactId>
    <version>1.4.6.RELEASE</version>
</dependency>
<!--Eureka: Ribbon需要从Eureka服务中心获取要拿什么-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
    <version>1.4.6.RELEASE</version>
</dependency>

在application.yaml文件中配置 Eureka

# Eureka 配置
eureka:
  client:
    register-with-eureka: false # 不向 Eureka 注册自己
    service-url: # 从三个注册中心随机取一个去访问
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/

主启动类加上@EnableEurekaClient注解,开启 Eureka

package com.zhou.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

//Ribbon 和 Eureka 整合以后,客户端可以直接调用,不用关心IP地址和端口号
@SpringBootApplication
@EnableEurekaClient //开启 Eureka 客户端
public class DeptConsumer_80 {
    public static void main(String[] args) {
        SpringApplication.run(DeptConsumer_80.class,args);
    }
}

自定义Spring配置类:ConfigBean.java 配置负载均衡实现RestTemplate

package com.zhou.springcloud.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration //@Configuration 相当于 spring中 applicationContext.xml
public class ConfigBean {
    //配置负载均衡实现RestTemplate
    @Bean
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
}

修改conroller:DeptConsumerController.java

//http://localhost:8081/dept/add?dname=地狱部
//private static final String REST_URL_PREFIX="http://localhost:8081";
private static final String REST_URL_PREFIX = "http://SPRINGCLOUD-PROVIDER-DEPT";
  • 使用Ribbon实现负载均衡

流程图:

1.参照springcloud-provider-dept-8001,新建两个服务提供者 Moudle:springcloud-provider-dept-8002、springcloud-provider-dept-8003 依次为另外两个Moudle添加pom.xml依赖 、resourece下的mybatis和application.yml配置,Java代码

2.启动所有服务测试,访问 http://eureka7001.com:7002/

七、Feign:负载均衡(基于服务端)

Feign是声明式Web Service客户端,它让微服务之间的调用变得更简单,类似controller调用service。SpringCloud集成了Ribbon和Eureka,可以使用Feigin提供负载均衡的http客户端。

Feign 主要是社区版,大家都习惯面向接口编程。这个是很多开发人员的规范。调用微服务访问两种方法

  • 微服务名字 【ribbon】
  • 接口和注解 【feign】

Feign旨在使编写Java Http客户端变得更容易。

前面在使用 Ribbon + RestTemplate 时,利用 RestTemplate 对 Http 请求的封装处理,形成了一套模板化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一个客户端类来包装这些依赖服务的调用。
所以,Feign 在此基础上做了进一步的封装,由他来帮助我们定义和实现依赖服务接口的定义,在 Feign 的实现下,我们只需要创建一个接口并使用注解的方式来配置它 (类似以前Dao接口上标注 Mapper 注解,现在是一个微服务接口上面标注一个 Feign 注解),即可完成对服务提供方的接口绑定,简化了使用 Spring Cloud Ribbon 时,自动封装服务调用客户端的开发量。

Feign默认集成了Ribbon

利用Ribbon维护了MicroServiceCloud-Dept的服务列表信息,并且通过轮询实现了客户端的负载均衡,而与Ribbon不同的是,通过Feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。

  1. 创建springcloud-consumer-fdept-feign模块

Feign的使用步骤

八、Hystrix:服务熔断

九、Zull路由网关

十、Spring Cloud Config 分布式配置

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

IT谢彪

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

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

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

打赏作者

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

抵扣说明:

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

余额充值