查看原文
其他

三万字 | SpringCloud-Netflix

树莓蛋黄派 Java规途 2023-07-04


  • 一、微服务与微服务架构

    • (一)什么是微服务的核心及相关问题

    • (二)微服务与微服务架构的辨析

    • (三)微服务技术栈

    • (四)为什么选择SpringCloud作为微服务架构?

  • 二、SpringCloud概述

    • (一)什么是SpringCloud

    • (二)SpringCloud的组成

    • (三)SpringBoot与SpringCloud的关系

    • (四)Dubbo与SpringCloud技术选型

    • (五) SpringCloud作用

    • (六)SpringCloud下载

    • (七)SpringCloud版本选择

  • 三、SpringCloud项目准备

    • (一)创建SpringCloud项目准备

    • (二) API网关模块

    • (三)服务提供者

    • (四)服务消费者

  • 四、Eureka服务注册与发现

    • (一)什么是Eureka

    • (二)Eureka原理

    • (三)创建Eureka注册中心

    • (四)修改服务提供者SpringCloud-provider-8001

    • (五)Eeureka集群配置

    • (六)对比Zookeeper

  • 五、负载均衡之Ribbon

    • (一)Ribbon简介

    • (二)服务消费方简单集成Ribbon

    • (三)Ribbon实现负载均衡

    • (四)Ribbon自定义算法实现

  • 六、Feign负载均衡

    • (一)Feign简介

    • (二)Feign的使用

  • 七、Hystrix服务熔断与服务降级

    • (一)Hystrix的简介

    • (二)服务熔断机制实现

    • (三)服务降级机制实现

    • (四)实现服务监控

  • 八、Zuul路由网关

    • (一)API网关(GateWay)

    • (二)Zuul概述

    • (三)Zuul路由实现

  • 九、SpringCloud config分布式配置

    • (一)分布式配置概述

    • (二)SpringCloud Config实现

    • (三)为Eureka和消费提供者配置远程配置

  • 往期内容回顾


SpringCloud

一、微服务与微服务架构

(一)什么是微服务的核心及相关问题

微服务的核心就是「将一些传统的一站式应用,根据业务拆分成一个一个的服务,彻底地去耦合」。每一个微服务提供单个业务功能的服务,一个服务做一件事情,从技术角度来看就是一种小而独立的处理过程,类似进程的概念,能够自行单独启动或销毁,拥有自己独立的数据库….

1. 核心的四个问题

  • 服务很多,客户端如何访问?
  • 服务很多,服务之间如何通信?
  • 服务很多。如何治理?
  • 服务器出现宕机,该如何处理?

2. 解决方案

  • Spring Cloud Netflix 一站式解决方案

    • api网关    zuul组件
    • Feign   – HttpClient  —Http的通信方式
    • 服务注册与发现:Eureka
    • 熔断机制:Hystrix
  • Apache Dubbo Zookeeper

    • API网关 :需要借助第三方组件或者自己实现
    • Dubbo 轻量级的RPC框架
    • 服务与注册:Zookeeper
    • 熔断机制:需要借助第三方的组件

Dubbo这个方案并不完善,这是一个专一的RPC框架。

  • Spring Cloud Alibaba 一站式解决方案 更加简单

新概念:服务网格——Server Mesh

istio

(二)微服务与微服务架构的辨析

1. 微服务

强调的是服务的大小,他关注的是某一个点:是具体解决某一个问题/提供落地对应服务的一个服务应用,狭义的看,可以看作是项目中的一个个微服务工程。

以IDEA为例,IDEA工具里面使用Maven开发的一个个独立的小Module,它具体是使用SpringBoot开发的一个小模块,专业的事情交给专业的模块来做,一个模块只做一件事。

强调的是一个个的个体,每个个体完成一个具体的任务或者功能。

2. 微服务架构

是一种架构形式

微服务架构是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调,互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务与服务之间采用轻量级的通信机制互相协作,每个服务都围绕着具体的业务进行构建,并且能够被独立的部署到生产环境中,另外,应该尽量避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据业务上下文,选择合适的语言、工具进行构建。

3. 微服务的优缺点

「优点:」

  • 每个服务足够内聚,足够小,代码容易理解,这样能聚焦一个指定的业务功能或业务需求。
  • 开发简单,开发效率提高,一个服务可能就是只专一地干一件事。
  • 微服务能够被小团队单独开发,这个小团队是2-5人的开发人员。
  • 微服务是松耦合的,是有功能意义的服务,无论是在开发阶段或部署阶段都是独立的。
  • 微服务能够使用不同的语言开发。
  • 易于和第三方集成,微服务允许容易且灵活的方式集成自动部署,通过持续集成工具,如jenkins、Hudson、banboo等
  • 微服务易于被一个开发人员理解,修改和维护,这样小团队能够更关注自己的工作成功,无需通过合作才能体现价值。
  • 微服务允许你利用融合最新技术。
  • 「微服务只是业务逻辑的代码,不会和HTML、CSS等其他界面混合。」
  • 「每个微服务都有自己的存储能力,可以有自己的数据库,也可以有统一数据库。」
  • 单一职责原则

「缺点」

  • 开发人员要处理分布式系统的复杂性
  • 多服务运维难度:随着服务的增加,运维的压力也在增大。
  • 系统部署依赖
  • 服务间通信成本
  • 数据一致性
  • 系统集成测试
  • 性能监控等…

(三)微服务技术栈

微服务条目落地技术
服务开发SpringBoot、Spring、SpringMVC
服务配置与管理Netflix公司的Archaius、阿里的Diamond等
服务注册与发现Eureka、Consul、Zookeeper等
服务调用Rest、RPC、gRPC等
服务熔断器Hystrix、Envoy等
负载均衡Ribbon、Nginx等
服务接口调用(客户端调用服务的简化工具)Feign等
消息队列Kafka、RabbitMQ、ActiveMQ等
服务配置中心管理SpringCloudConfig、Chef等
服务路由(API网关)Zuul等
服务监控Zabbix、Nagios、Metrics、Specatator等
全链路追踪Zipkin、Brave、Dapper等
服务部署Docker、OpenStack、Kubernetes等
数据流操作开发包SpringCloud Stream(封装与Redis、Rabbit、Kafka等发送接收消息)
事件消息总线SpringCloud Bus

(四)为什么选择SpringCloud作为微服务架构?

1. 选型依据

  • 整体解决方案和框架成熟度
  • 社区热度
  • 可维护性
  • 学习曲线

2. 当前各大IT公司的微服务架构

  • 阿里:Dubbo+HFS
  • 京东:JSF
  • 新浪:Motan
  • 当当网:DubboX
  • ……..

3. 各微服务框架对比

功能点/服务框架Netflix/SpringCloudMotangRPCThriftDubbo/DubboX
功能定位完整的微服务框架RPC框架,但整合了ZK或Consul,实现集群环境的基本服务注册/发现RPC框架RPC框架服务框架
支持Rest是,Ribbon支持多种可插拔的序列化选择
支持RPC是(Hession2)
支持多语言是(Rest形式)
负载均衡是(服务端zuul+客户端Ribbon),zuul-服务、动态路由、云端负载均衡Eureka(针对中间层服务器)是(客户端)是(客户端)
配置服务Netflix Archaius、Spring  Cloud Config Server集中配置是(Zookeeper提供)
服务调用链监控是(zuul),zuul提供边缘服务,API网关
高可用/容错是(服务端Hystrix+客户端Ribbon)是(客户端)是(客户端)
典型应用NetflixSinaGoogleFaceBook
社区活跃度一般一般2017重新开始维护,之间中断5年
学习难度中等
文档丰富程度一般一般一般
其他Spring Cloud Bus为我们的应用程序带来了更多的管理端点支持降级Netflix在内部开发集成gRPCIDL定义实践公司较多

二、SpringCloud概述

(一)什么是SpringCloud

Spring Cloud是一系列框架的「有序集合」。它利用Spring Boot的开发便利性巧妙地简化了「分布式系统基础设施的开发」,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。

Spring Cloud并没有重复制造轮子,它只是将各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理。

最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。

Spring Cloud并不是一个框架,而是生态。

(二)SpringCloud的组成

Spring Cloud的子项目,大致可分成两类。

一类是对现有成熟框架Spring Boot化”的封装和抽象,也是数量最多的项目;

第二类是开发了一部分「分布式系统的基础设施」的实现,如Spring Cloud Stream扮演的就是kafka, ActiveMQ这样的角色。

对于我们想快速实践微服务的开发者来说,第一类子项目就已经足够使用:

  • 「Spring Cloud Netflix」 : 是对Netflix开发的一套分布式服务框架的封装,包括服务的发现和注册,负载均衡、断路器、REST客户端、请求路由等。
  • 「Spring Cloud Config」 : 将配置信息中央化保存, 配置Spring Cloud Bus可以实现动态修改配置文件
  • 「Spring Cloud Stream」 : 分布式消息队列,是对Kafka, MQ的封装
  • 「Spring Cloud Security」 : 对Spring Security的封装,并能配合Netflix使用
  • 「Spring Cloud Zookeeper」 : 对Zookeeper的封装,使之能配置其它Spring Cloud的子项目使用
  • 「Spring Cloud Eureka」  :Spring Cloud Netflix 微服务套件中的一部分,它基于Netflix Eureka 做了二次封装,主要负责完成微服务架构中的服务治理功能。

(三)SpringBoot与SpringCloud的关系

  • SpringBoot专注于快速方便的开发单个个体微服务
  • SpringCloud是关注全局的微服务协调整理治理框架,它将SpringBoot开发的一个个单体微服务整合并管理起来,为各个微服务之间提供:配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等集成服务。
  • SpringBoot可以离开SpringCloud独立使用,开发项目,但是SpringCloud离不开SpringBoot,属于依赖关系
  • 「SpringBoot专注于快速、方便的开发单个个体微服务。SpringCloud专注于全局的服务治理框架。」

(四)Dubbo与SpringCloud技术选型

1. 分布式+服务治理Dubbo

目前成熟的互联网架构:应用服务化拆分和消息中间件

2. Dubbo和SpringCloud的对比


DubboSpringCloud
服务注册中心ZookeeperSpringCloud Netflix Eureka
服务调用方式RPCREST API
服务监控Dubbo-monitorSpringBoot Admin
断路器不完善SpringCloud Netflix Hystrix
服务网关SpringCloud Netflix zuul
分布式配置SpringCloud Config
服务跟踪SpringCloud Sleuth
消息总线SpringCloud Bus
数据流SpringCloud Stream
批量任务SpringCloud Task

「最大区别在于:SpringCloud抛弃了Dubbo的RPC通信,采用的是基于HTTP的REST方式。」

两种方式各有优劣,后者牺牲了服务调用的性能,但也避免了原生RPC带来的问题,而且REST相比RPC更加灵活,服务提供方和调用方只是依赖一纸契约,不存在代码级别的强依赖,这在强调快速演化的微服务环境下,显得更加合适。

(五) SpringCloud作用

  • Distributed/versioned configuration (分布式/版本控制配置)
  • Service registration and discovery(服务注册与发现)
  • Routing(路由)
  • Service-to-Service calls(服务到服务的调用)
  • Load balancing (负载均衡配置)
  • Circuit Breakers(断路器)
  • Distributed messaging(分布式消息管理)
  • …..

(六)SpringCloud下载

SpringCloud官网:https://spring.io/projects/spring-cloud#learn

  • SpringCloud是一个由众多独立子项目组成的大型综合项目,每个子项目有不同的发行节奏。都维护着自己的发布版本号,SpringCloud通过一个资源清单BOM来管理每个版本的子项目清单,为避免与子项目的发布号混淆,所以没有采用版本号的方式,而是通过命名的方式。
  • 这些版本名称的命名方式采用了伦敦地铁站的名称,同时根据字母的顺序来对应版本的时间顺序。

(七)SpringCloud版本选择

SpringBootSpring Cloud关系
1.2.XAngel 版 本 (天 使 )兼容 Spring Boot 1.2.X
1.3.XBrixton 版 本 ( 布 里 克 斯 顿 )兼容 Spring Boot 1.3.x, 也 兼容 Spring Boot 1.4.x
1.4.XCamden 版 本 ( 卡 姆 登 )兼容 Spring Boot 1.4.Xx, 也 兼容 Spring Boot 1.5.X
1.5.XDalston 版 本 (多 尔 斯 顿 )兼容 Spring Boot 1.5.x, 不 兼容 Spring Boot 2.0.X
1.5.XEdgware 版 本 ( 埃 奇 韦 尔 )兼容 Spring Boot 1.5.x, 不 兼容 Spring Boot 2.0X
2.0.XFinchley 版 本 ( 芬 奇 利 )兼容 Spring Boot 2.0.x,不兼容SpringBoot1.5.X
2.1.XGreenwich 版 本 (格林 威 治 )\

三、SpringCloud项目准备

(一)创建SpringCloud项目准备

  • 打开Idea创建一个普通Maven项目
  • 将父项目命名为SpringCloud(作为一个总工程)
  • 导入相应的依赖
 <!--    定义版本号-->
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <junit.version>4.12</junit.version>
        <lombok.version>1.18.12</lombok.version>
        <log4j.version>1.2.17</log4j.version>
    </properties>

<!--    打包方式从jar包变为pom-->
    <packaging>pom</packaging>

<!--    依赖管理-->
    <dependencyManagement>
        <dependencies>
<!--            springcloud的依赖-->
            <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-dependencies -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.SR3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

<!--            导入springboot的依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.1.8.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

<!--            数据库-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.16</version>
            </dependency>
<!--            数据源-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.2.8</version>
            </dependency>
<!--            springboot启动器-->
<!--            使用mybatis的整合-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>1.3.2</version>
            </dependency>
<!--            单元测试-->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <!--此处直接引用junit的版本-->
                <version>${junit.version}</version>
                <scope>test</scope>
            </dependency>
<!--            使用lombok-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <!--此处直接引用lombok的版本-->
                <version>${lombok.version}</version>
            </dependency>
<!--            日志依赖-->
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>${log4j.version}</version>
            </dependency>
            <dependency>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-core</artifactId>
                <version>1.2.9</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

此处需要注意的是dependencyManagement表示的是管理依赖,子项目需要的时候才会从父项目中导入依赖。

(二) API网关模块

  1. 创建一个新的模块,命名为api,也是普通的maven项目
  1. 导入相关的依赖
   <dependencies>
<!--        lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>
  1. 创建数据库
CREATE DATABASE DB_API;
USE DB_API;
CREATE TABLE dept(
    deptno bigint NOT NULL PRIMARY KEY AUTO_INCREMENT,
    dname VARCHAR(60NOT NULL ,
    db_source VARCHAR(60)
);
ALTER TABLE dept COMMENT ='部门表';
-- 使用数据库函数,读取数据库
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());
  • 首先创建了一个网关API数据库,并且创建了一个部门表,部门表内主要存储的是部门的编号,部门的名字以及所对应的数据库资源(即存放在哪个数据库)
  • DATABASE()默认返回当前数据库的名称。
  1. 创建部门所对应的实体类
@Data
@NoArgsConstructor
@Accessors(chain = true)
public class Dept implements Serializable {
    /**SpringCloud中的所有的实体类都必须要实现序列化*/
    private long deptno;
    private String dname;
    /**
     * 查看某个数据是存在哪个数据库的字段
     */

    private String db_source;

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

    /**
     * @Accessors(chain = true)
     * 表示链式写法*/

}
  • 首先该实体类与数据库中的表存在关系映射,即ORM关系映射。
  • 其次构造方法上,只需要传入name即可,因为编号默认生成了,数据库资源使用函数来自动生成。
  • 最后使用lombok来自动生成对应的setter和getter方法,并且使用注解@Accessors(chain = true)支持了链式写法。如下所示:
Dept dept=new Dept();
dept.setDbno(xxx).setDbname(xxx).setDbsource(xx);

支持API网关的相关配置已经完成了。

(三)服务提供者

  1. 创建一个新的module(普通maven项目),命名为SpringCloud-provider-8001
  2. 导入服务提供者所需要的依赖(所提供的服务主要是部门服务)
<!--        需要使用到API中的实体类,因为这是服务提供者,需要提供部门有关服务-->
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>SpringCloud-API</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

<!--        需要使用单元测试-->
        <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>

<!--        mybatis整合springboot的相关依赖-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
<!--        导入springboot的相关测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-test</artifactId>
        </dependency>
<!--        导入springboot-web的依赖-->
        <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>
  • 需要注意的是添加的第一个坐标依赖是API项目的有关依赖,因为服务的提供者需要提供有关部门的服务,这就需要相应的部门实体类作为支持。
  • 导入了web的依赖支持
  • 使用了另一个应用服务器jetty并导入了热部署的相关依赖。
  1. 编写相关配置

在resources目录下编写有关的配置application.yaml

# 设置服务器启动的端口号
server:
  port: 8001

# 编写mybatis的有关配置
mybatis:
  type-aliases-package: com.example.springcloud.pojo
  config-location: classpath:/mybatis/mybatis-config.xml
  mapper-locations: classpath:/mybatis/mapper/*.xml
# 编写spring的有关配置  
spring:
  application:
    name: springcloud-provider-dept
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/DB_API?useUnicode=true&characterEncoding=utf-8
    username: root
    password: 123456
  • 设置相关服务器的启动端口号为8001,这是为了防止出现在单机情况下的端口号的冲突问题。
  • 编写mybatis的有关配置,首先设置了需要扫描的哪个包(此包在另一个module中,因为我们导入了有关的依赖,所以实际上可以访问到),需要引用另一个module中的实体类。之后是mybatis的核心配置文件所在的类路径(此处需要我们在resources中创建一个mybatis文件夹,并新建对应的核心文件)。最后需要配置有关mapper文件的所在位置(此处需要在mybatis包的基础上新建mapper包,将所有的xxxmapper.xml文件放置于此)。
  • 之后编写spring的有关配置。首先是设置服务提供者的名字(此处以项目的名称为服务提供者的名字),之后配置数据源(我们导入了德鲁伊的数据源),最后设置连接数据库的驱动名、url地址、用户名以及密码。
  1. 编写mybatis的核心配置文件
<?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>
  • 此处只需要对开启二级缓存进行设置即可。
  • 之前进行数据源的有关设置以及别名的设置在yaml配置文件中已经进行过相关的设置。
  1. 编写持久层dao

创建一个数据库操作接口,定义了增查的相关方法

@Mapper
@Repository
public interface DeptMapper {
    /**
     * Query all dept list.
     *查询所有的部门,返回一个部门的list集合
     * @return the list
     */


    List<Dept> queryAllDept();

    /**
     * Add dept boolean.
     *添加一个部门,最后返回结果的布尔值
     * @param dept the dept
     * @return the boolean
     */

    boolean addDept(Dept dept);

    /**
     * Query dept by id dept.
     *通过id查询一个部门
     * @param id the id
     * @return the dept
     */

    Dept queryDeptById(long id);

}

  • @Mapper的作用是将这个接口标记为一个映射接口。
  • @Repository用在持久层的接口上,这个注解是将接口的一个实现类交给spring管理。
  • 编写了三个方法,主要是对已经建立的数据库进行相关的操作。
  1. 编写持久层的实现
<?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.provider.springcloud.dao.DeptMapper">
    <insert id="addDept" parameterType="com.example.springcloud.pojo.Dept">
        INSERT INTO DB_API.dept(dname, db_source)
        VALUES(#{dname},DATABASE());
    </insert>

    <select id="queryAllDept" resultType="com.example.springcloud.pojo.Dept">
        SELECT * FROM DB_API.dept;
    </select>

    <select id="queryDeptById" resultType="com.example.springcloud.pojo.Dept">
        SELECT * FROM DB_API.dept WHERE deptno=#{deptno};
    </select>
</mapper>
  • 此处主要的目的是编写持久层接口DeptMapper的有关实现。
  • 同样的我们即使不使用xml文件的形式来实现该接口,也可以在接口的域上使用相应的注解来实现。如下所示
 @Select("SELECT * FROM DB_API.dept")
    List<Dept> queryAllDept();

 @Insert(" INSERT INTO DB_API.dept(dname, db_source) VALUES(#{dname},DATABASE())")
    boolean addDept(Dept dept);

 @Select(" SELECT * FROM DB_API.dept WHERE deptno=#{deptno}")
    Dept queryDeptById(long id);

只不过需要注意的是xml和注解的方式二选一即可。

  1. 编写服务提供者的服务层的有关接口与实现

「服务层的接口」

public interface DeptService {
    /**
     * Query all dept list.
     *查询所有的部门,返回一个部门的list集合
     * @return the list
     */


    List<Dept> queryAllDept();

    /**
     * Add dept boolean.
     *添加一个部门,最后返回结果的布尔值
     * @param dept the dept
     * @return the boolean
     */

    boolean addDept(Dept dept);

    /**
     * Query dept by id dept.
     *通过id查询一个部门
     * @param id the id
     * @return the dept
     */

    Dept queryDeptById(long id);

}

此处服务层的接口与持久层的接口的方法是一致的

「服务层接口实现类」

@Service
public class DeptServiceImpl implements DeptService{

    private DeptMapper deptMapper;

    @Autowired
    public DeptServiceImpl(DeptMapper deptMapper) {
        this.deptMapper = deptMapper;
    }

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

    @Override
    public boolean addDept(Dept dept) {
        return deptMapper.addDept(dept);
    }

    @Override
    public Dept queryDeptById(long id) {
        return deptMapper.queryDeptById(id);
    }
}
  • 此处添加@Service注解将该类标记为服务层的bean交给spring进行管理。
  • 此处使用构造器注入持久层的依赖deptMapper,同时使用@Autowired注解完成注入
  • 最后通过持久层对象调用对应的方法返回结果。
  1. 编写控制层类
@RestController("/dept")
public class DeptController {
    private DeptService deptService;
    @Autowired
    public DeptController(DeptService deptService) {
        this.deptService = deptService;
    }
    /**
     * 编写添加方法
     * @param dept
     * @return
     */

    @PostMapping("/add")
    public boolean addDept(@RequestBody Dept dept){
        return deptService.addDept(dept);
    }
    /**
     * 使用restful风格的写法获取单个部门
     * @param id
     * @return
     */

    @GetMapping("/get/{id}")
    public Dept get(@PathVariable("id") Long id){
        return deptService.queryDeptById(id);
    }
    /**
     * 查询所有的部门信息
     * @return
     */

    @GetMapping("/list")
    public List<Dept> queryAll(){
        return deptService.queryAllDept();
    }
}
  • 此处使用@RestController("/dept")表示此处并不涉及到页面的跳转,只负责传递有关的信息
  • 该控制层类使用Restful风格的有关服务。
  • 一般提供任何的东西都需要使用@PostMapping,一般获取某一些内容都使用@GetMapping
  • @GetMapping("/get/{id}")是一个标准的RestFul风格的请求。
  1. 编写主启动类
@SpringBootApplication
public class DeptProvider {
    public static void main(String[] args) {
        SpringApplication.run(DeptProvider.class,args);
    }
}
  • 主启动类可以自定义,需要添加注解@SpringBootApplication
  • 编写main方法
  • 使用SpringApplication类调用静态方法run,传入参数,主启动类的Class对象,并传入主方法的参数args
  • SpringBoot项目创建完成
  1. 测试访问

访问:http://localhost:8001/list会返回如上的Json字符串,表示已经从数据库中获取到指定的字符串。

(四)服务消费者

  1. 新建一个新的module(普通maven项目),命名为SpringCloud-consumer-8080
  2. 导入服务消费者所需要的相关依赖
<!--实体类的有关依赖-->
  <dependency>
      <groupId>org.example</groupId>
      <artifactId>SpringCloud-API</artifactId>
      <version>1.0-SNAPSHOT</version>
  </dependency>

  <!--web支持-->
  <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>

由于消费者只需要关注web支持和实体类支持,而不需要连接数据库以及设置日志、整合mybatis等操作。所以只需要导入实体类的依赖以及web支持和热部署工具。

  1. 编写相应的配置文件
# 配置端口号
server:
  port: 8080

# 配置消费者的名字
spring:
  application:
    name: springcloud-consumer-dept
    
   # 配置热部署的端口
  devtools:
    livereload:
      port: 32961   
  • 一般来说,消费者服务的端口一般是http的默认端口80。所以可以设置为80端口,并设置消费者的名字。但由于存在端口占用的问题,所以最终将端口号设置为8080.
  • 重新设置另一个devtools的端口,防止运行多个application的时候发生冲突,导致警告
  1. 将RestTemplate注入容器
@Configuration
public class BeanConfig {
    
    @Bean
    public RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
    
}
  • RestTempalte是一个SpringBoot为我们封装好的处理RestFul风格请求的模板类,但由于SpringBoot并没有将其注入到容器中,所以需要自行配置。
  • @Configuration注解表明这是一个配置类。类似于Spring中原生的ApplicationContext.xml文件,一般的bean对象都可以在有该注解的类下进行相应的配置,也就是使用Java语言的方式来配置bean。
  • @Bean表明将会把该方法所返回的对象注入到容器中,交由IOC容器管理,对应的返回的对象成为Spring中的bean对象。
  1. 编写控制层方法
@RestController
public class DeptConsumerController {

    /**
     * 注入处理RestFul风格请求的模板类
     */

    @Resource
    private RestTemplate restTemplate;

    /**
     * 由于需要从远程的服务提供者获取相应的资源,所以需要设置远程提供者资源请求的固定前缀
     */

    private static final String REST_URL_PREFIX = "http://localhost:8001";


    /**
     * 通过该方法远程获取指定id的Dept对象
     * 获取对象使用get方式
     *
     * @param id
     * @return
     */

    @RequestMapping("/getDept/{id}")
    public Dept getDeptById(@PathVariable("id") Long id) {
        //此处需要使用到restTemplate对象的方法,该方法需要传入远程的资源地址以及返回值的Class类型
        //此处url地址的书写需要和远程服务端的controller请求的地址保持一致。
       return restTemplate.getForObject(REST_URL_PREFIX + "/get/" + id, Dept.class);
    }

    /**
     * 通过addDept方法提交一个内容,使用post方式请求
     * @param dept
     * @return
     */

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

    /**
     * 获取所有的内容,远程调用依旧是get方式
     * @return
     */

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

}

  • 适用RestController接口表示该控制类在处理Restful风格请求的时候
  • BeanConfig中定义的RestTemplate通过@Resource注解注入到控制类中。
  • REST_URL_PREFIX常量的定义是因为,消费者需要远程访问服务者提供的有关数据查询的服务,需要访问对应的请求,但是不论请求怎样,前缀是固定的,也就是服务者提供的所有请求都需要通过前缀地址为http://localhost:8001的地址来进行相应的访问,只是根据业务的不同需要加入不同的后缀,也是采用RestFul风格的请求。
  • 接下来定义了三个不同的方法,这三个方法的请求的形式都是通过@RequestMapping来定义的,这就说明在消费者这一端,无论是采用什么样的方式来操作都可以,具体去访问服务者的哪个请求在方法体中已经进行了相应的声明。例如执行post请求就使用方法postForObject,执行get请求就使用方法getForObject

API网关设置、服务提供者与服务消费者编写中出现的问题:

  1. 出现Unable to start embedded Tomcat server

主要是因为在使用SpringBoot 热部署插件 devtools ,同时启动多个Application时,控制台会报这个警告;问题在于:DevToolsProperties中配置了一个端口,默认是35729,因此我们需要在另一个模块中添加一个新的端口号,只要新端口号与默认端口号不同且不在1024以内即可。

  1. 启动端口号为80的项目时候出现Unable to start embedded Tomcat server

这是因为端口号80属于1024以内,没有足够的权限去使用访问该端口号,也可能是因为存在端口的占用问题,所以出现了无法启动服务器的情况。只需要将端口号修改为大于1024的即可正常访问。

  1. 在消费端(该添加方法使用的是@RequestMapping)利用get方式提交请求参数的时候出现了500的错误

这是因为在消费者的添加请求的方法体中使用的是post的方式提交浏览器传递的参数,但在浏览器的地址栏中提交了显式的参数,导致500错误

此时为了能够正确地提交,需要在服务提供者执行添加方法的参数前加入@RequestBody注解,就可以正常提交了。

 /**
     * 编写添加方法
     * @param dept
     * @return
     */

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

并且 @RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的);而最常用的使用请求体传参的无疑是POST请求了,所以使用@RequestBody接收数据时,一般都用POST方式进行提交。

四、Eureka服务注册与发现

(一)什么是Eureka

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

(二)Eureka原理

1. Eureka的基本架构

  • SpringCloud封装了Netflix公司开发的Eureka模块来实现服务注册与发现。
  • Eureka采用了C/S的架构设计,EurekaServer作为服务注册功能的服务器,即服务注册中心。
  • 系统中的其他微服务,使用Eureka的客户端连接到EurekaServer并维持连接。这样系统的维护人员就可以通过EurekaServer来监控系统中的各个微服务是否正常运行,SpringCloud的一些其他模块(比如Zuul)就可以通过EurekaServer来发现系统中的其他微服务,并执行相关的逻辑。
  • Eureka服务流程图
  • 流程图解读
    • Eureka包含两个组件,「Eureka Server」「Eureka Client」
    • Eureka Server提供注册服务,各个节点启动之后,会在Eureka Server中进行注册,这样Eureka Server的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观地看到。
    • Eureka Client是一个Java客户端,用于简化Eureka Server的交互客户端同时也具备一个内置的,使用轮询负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除掉,默认周期为90秒。
  • 「三大角色」
    • 「Eureka Server」:提供服务的注册与发现
    • 「Service Provider」:将自身服务注册到Eureka中
    • 「Service Consumer」:服务消费方从Eureka中获取注册服务列表,从而找到消费服务。

2. Eureka自我保护机制

即:「某一时刻某一个微服务不可用了,Eureka不会立刻进行清理,而是依旧会对该微服务信息进行保存。」

  • 默认情况下,如果Eureka在一定的时间内没有接收到某个微服务实例的心跳。EurekaServer将会注销该实例(默认90秒)。但是当网络分区故障发生时,  微服务与Eureka之间无法正常通行 ,以上行为可能变得非常危险了,因为微服务本身其实是健康的 ,此时本不应该注销这个服务  Eureka 通过自我保护机制来解决这个问题。当EurekaServer节点在短时间内丢失过客户端时(例如发生网络分区故障),那么这个节点就会进入自我保护模式,一旦进入该模式,EurekaServer就会保护服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务),当网络故障恢复之后,该EurekaServer节点会自动退出自我保护模式。
  • 在自我保护模式中,EurekaServer会保护服务注册表中的信息,不再注销任何服务实例,当它收到的心跳数重新恢复到阈值以上时,该EurekaServer节点就会自动退出自我保护模式,它的设计哲学就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。
  • 自我保护模式「是一种应对网络异常的安全保护措施」。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留),也不盲目注销任何健康的微服务。这种模式,可以让Eureka集群更加的健壮和稳定。
  • 在SpringCloud中可以适用eureka.server.enable-self-preservation=false禁用自我保护模式,并且是「不推荐关闭自我保护模式」

(三)创建Eureka注册中心

在SpringCloud项目准备的基础上,添加Eureka注册中心服务。

  1. 新建一个Module,命名为SpringCloud-eureka-7001
  2. 导入相关的依赖

在maven仓库中,大致有3种依赖

首先必须建立Eureka的服务中心。

 <!-- 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>3.0.4</version>
        </dependency>
<!--        热部署工具-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>

注意新版的必须要选择带spring-cloud-starter-netflix-eureka-server

  1. 编写配置文件
server:
  port: 7001

# Eureka配置
eureka:
  instance:
    hostname: localhost  #Eureka服务端的实例名称(服务端主机名为localhost)
  client:
    register-with-eureka: false    # 表示是否向Eureka注册中心注册自己(这里是服务端,为false)
    fetch-registry: false  #表示如果fetch-register为false表示自己为服务中心
    service-url:  #类似于监控页面
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka #这里使用${}取出相关设置,用于服务端的访问地址
  • 设置了Eureka服务端的默认端口号为7001
  • 设置Eureka的实体配置主机名为localhost(本机)
  • 最后需要注意的是service-url默认的访问地址是依据配置文件不断变化的。
  1. 编写主启动类
@SpringBootApplication
@EnableEurekaServer
public class EurekaServer {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServer.class,args);
    }
}

  • @EnableEurekaServer表示服务端的启动类,可以接受其他注册进来。
  • @SpringBootApplication表明这是一个SpringBoot的应用
  1. 测试访问,但是报错了

出现了异常org.springframework.boot.context.properties.ConfigurationBeanFactoryMetadata

这个异常表示「SpringBoot的版本与SpringCloud的版本不一致导致的。」

对此我们需要切换相应的版本。

由于使用的SpringCloud版本是Greenwich.SR3版本,可以将SpringBoot的版本调整为2.1.8.RELEASE

对应的版本对应关系在Spring官网可查:https://spring.io/projects/spring-cloud

  1. 修改版本后再次访问,访问成功!

(四)修改服务提供者SpringCloud-provider-8001

1. 添加到注册中心

(1)首先添加相应的Eureka依赖

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
            <version>1.4.6.RELEASE</version>
</dependency>

(2)修改服务提供者的配置

# 配置eureka
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka

因为我们需要将provider的服务进行注册,需要提供Eureka注册中心的ip地址,而该地址是在Eureka注册中心的配置文件中的地址。所以配置的格式是一样的,也是上面这样配置。

(3)开启注解支持

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

@EnableEurekaClient这个注解表明一旦服务启动,就会将该提供者provider作为Eureka的客户端注册到Eureka注册中心中进行管理。

(4)测试注册

  • 首先开启Eureka的注册中心服务。
  • 再开启服务提供者的服务
  • 访问http://localhost:7001即可查看服务提供者有无被注册到Eureka注册中心中。

如上图所示在Application中有服务提供者,表明服务提供者已经被注册到Eureka服务中心中。

  • 上图中的状态:10.200.1.212-springcloud-provider-dept:8001可以在提供者的配置文件中进行修改。
# 配置eureka
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
  instance:
    instance-id: java-spring-cloud
 # 在服务提供者的yaml配置文件中进行配置instace-id   

如图所示修改成功

  • 上图中的Application的名字就是在服务提供者的配置文件中配置的spring.application.name,可以进行相应的修改。

  • 如果中途停止服务提供者的服务,Eureka注册中心中依旧会保持服务提供者的注册数据,但是由于「心跳机制」,过了一定的时间周期便会取消注册。

  • 「状态信息中的链接是可以进行访问的,但是需要进行配置,链接的内容是SpringCloud中服务提供者的加载的相关信息。」

2. 添加服务提供者的监控信息

(1)添加有关依赖

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

(2)配置相关信息

# 配置info    
info:
  app.name: demo for Spring Cloud  # 配置app的名字
  company.name: GPNU # 配置公司的名字

(3)测试

  • 首先开启Eureka注册中心服务
  • 再次开启服务提供者的有关服务
  • 测试访问http://localhost:7001,即可查看有关信息
  • 点击状态信息中的链接即可查看在配置文件中设置的info等有关信息。并且设置的信息都是以json字符串的格式来进行展示。

3. 通过DiscoveryClient获取注册信息

通过DiscoveryClient获取注册信息并不是必须的,不使用也不影响项目的运行,但在团队开发中,DiscoverClient使用可以很好地获取信息。

(1)在控制层编写获取信息的方法

    @Resource
    private DiscoveryClient discoveryClient;

    /**
     * 通过url获取一些info的注册信息
     * 可以通过该方法获取注册到Eureka服务中心的一些客户端信息
     * @return
     */

    @GetMapping("/dicovery")
    public Object discovery(){
        //获取所有微服务列表清单
        List<String> services = discoveryClient.getServices();
        System.out.println("所有的微服务列表:"+services);
        //获取所有的信息实例
        List<ServiceInstance> instances = discoveryClient.getInstances("SPRINGCLOUD-PROVIDER-DEPT");
        for (ServiceInstance instance : instances) {
            System.out.println(instance.getUri()+","+instance.getHost()+","+instance.getPort());
        }
        return this.discoveryClient;
    }
  • DiscoverClient是SpringCloud下的一个接口,其下有3个实现类,其中有一个包含EurekaDicoveryClient,主要是针对Eureka来实现的。(需要注意的是DiscoverClient有两个,一个是接口,是由SpringCloud提供的,一个是类,由Netflix提供的,这里使用的是SpringCloud提供的。)
  • @Resource表示将DiscoveryClient注入,获取其实例化的对象。
  • 通过DiscoveryClient对象调用方法获取微服务的相关信息。可以获取所有微服务的有关列表以及相关信息的实例化内容。例如:uri、端口、主机名等等。

(2)配置主启动类

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

添加注解@EnableDiscoveryClient支持。主要用于服务发现。

(3)测试获取信息

  • 首先运行EurekaServer的服务
  • 接着运行服务提供者的有关服务
  • 最后访问:http://localhost:8001/discovery即可获取到有关微服务的有关信息

并且也在控制台进行了有关信息的输出与回显。

(五)Eeureka集群配置

  1. 新建2个注册中心

复制原先的Eureka注册中心的有关设置,新添加2个注册中心,一个端口号为7002,另一个端口号为7003.基本配置与7001端口的注册中心设置一样。

  1. 进行相关配置

如下图所示,注册中心的有关注册数据需要进行相关的同步,以防止因为某些意外情况造成的数据丢失。

  • 由于需要进行单机集群的配置,所以需要进行「域名映射」的有关配置
sudo su  # 切换到管理员
vim /etc/hosts

# 新添加的域名映射
127.0.0.1    eureka7001.cn
127.0.0.1    eureka7002.cn
127.0.0.1    eureka7003.cn

保存即可。

  • 「修改各个注册中心的配置」

由于 需要配置单机注册集群,所以配置文件需要修改两个地方,「一个是主机名(新设置的域名映射),一个是关联其他的注册中心。」

「配置7001注册中心」

server:
  port: 7001

# Eureka配置
eureka:
  instance:
    hostname: eureka7001.cn  #Eureka服务端的实例名称(服务端主机名为eureka7001.cn)
    .......
    service-url: # 关联其他的注册中心
     defaultZone: http://eureka7002.cn:7002/eureka, http://eureka7003.cn:7003/eureka

「配置7002注册中心」

server:
  port: 7002

# Eureka配置
eureka:
  instance:
    hostname: eureka7002.cn  #Eureka服务端的实例名称(服务端主机名为eureka7002.cn)
    .......
    service-url:  # 关联其他的注册中心
     defaultZone: http://eureka7001.cn:7001/eureka, http://eureka7003.cn:7003/eureka
     

「配置7003注册中心」

server:
  port: 7003

# Eureka配置
eureka:
  instance:
    hostname: eureka7003.cn  #Eureka服务端的实例名称(服务端主机名为eureka7003.cn)
    ........
    service-url:   # 关联其他的注册中心
     defaultZone: http://eureka7002.cn:7002/eureka, http://eureka7001.cn:7001/eureka
  1. 进行服务的发布

也就是修改服务提供者的注册地址,原先的是到7001端口的注册中心进行服务注册,现在需要修改为向3台集群注册中心都进行注册。

# 配置eureka
eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.cn:7001/eureka,http://eureka7002.cn:7002/eureka, http://eureka7003.cn:7003/eureka

即将服务提供者的defaultZone注册地址添加为3个集群的注册地址。

  1. 进行单机集群测试

进行相应的测试。

  • 当访问任一一个注册中心的地址的时候(本次以http://eureka7001.cn:7001为例),都会在DB Replicas中显示其他注册中心的相关信息。
  • 并且注册到注册中心的客户端都会在所有注册中心中进行同步显示。

(六)对比Zookeeper

1.CAP原则

CAP原则是非关系型数据库所适用的原则(关系型数据库主要适用ACID原则)

CAP原则的三方面

  • 「C(Consistency)一致性」:在分布式环境中,一致性是指数据在多个副本之间是否能够保持一致的特性,等同于所有节点访问同一份最新的数据副本。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保证系统的数据仍然处于一致的状态。
  • 「A(Availability)可用性:」每次请求都能获取到正确的响应,但是不保证获取的数据为最新数据。
  • 「P(Partition tolerance)分区容错性:」分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。

2. CAP理论的核心

  • 一个分布式系统不可能同时很好地满足一致性、可用性与分区容错性这三个要求。

  • 根据CAP原理,将NoSQL数据库分成了满足CA原则,满足CP原则,满足AP原则三大类:

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

在真实的环境中(分布式环境),这三个基本需求中,最多只能同时满足其中的两项,P 是必须的,因此只能在 CP 和 AP 中选择,Eureka实现的AP原则,Zookeeper实现的是CP原则。

3. Zookeeper与Eureka的对比

由于一个分布式系统中不可能同时满足CAP三个原则,且由于在分布式系统中,分区容错性是必不可少的,因此,只能从CP或者AP中选择。并且「Zookeeper与Eureka的原则也是不同的。」

(1)Zookeeper保证CP

当注册中心查询到服务列表的时候,可以允许注册中心返回几分钟以前的注册信息,但不能接受服务直接宕掉,即:「服务注册功能对一致性的要求要高于可用性。」Zookeeper会出现这样一种情况:master节点故障与其他节点失去联系的时候,剩余节点会重新选举leader,(类似于redis集群中的哨兵模式)但由于选举的leader时间过长,并且在选举期间整个集群是不可用的,极容易导致选举期间注册服务瘫痪。(即可用性发生了问题)

(2)Eureka保证AP

Eureka在设计的时候优先保证了「可用性」「Eureka各个节点都是平等的」,几个节点的宕机不会影响其他节点的使用,剩余的节点依旧可以保证提供注册与查询服务。而Eureka的客户端在向某个节点注册时,如果发现失败了,则会自动切换到其他的节点。只要还有一台Eureka注册中心存在,就能保证注册服务的可用性,不过大概率会存在查询到的信息不是最新的。因此,「Eureka对于服务可用性的要求要高于一致性。」并且Eureka还有一种自我保护机制,如果在15min内超过85%的节点没有正常的心跳,那么Eureka就会认为客户端与注册中心发生了网络故障,此时会出现以下情况:

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

综上所述:Eureka可以很好地应对因网络故障导致部分节点失去联系的情况,不会像Zookeeper使整个注册服务瘫痪。

五、负载均衡之Ribbon

负载均衡:将负载(工作任务)进行平衡、分摊到多个操作单元上进行运行

(一)Ribbon简介

1. 什么是Ribbon

  • SpringCloud Ribbon是基于Netflix Ribbon实现的一套「客户端负载均衡工具」
  • Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将Netflix的中间层服务连接在一起。Ribbon的客户端组件提供一系列完整地配置项如:连接超时,重试等等。简单的说,就是在配置文件中列出LoadBalancer(简称LB,负载均衡)后面所有的机器,Ribbon会自动帮助使用者基于某种规则(如简单轮询、随机连接等等)去连接这些机器。同样使用者也很容易可以使用Ribbon实现自定义的负载均衡算法。

2. Ribbon作用

  • LB,「即负载均衡」,在微服务或分布式集群中经常用的一种应用。
  • 负载均衡简单来说就是将用户的请求平摊到多个服务上,从而达到系统的高可用。
  • 常见的负载均衡软件有Nignx,Lvs等等。
  • dubbo、SpringCloud中均给我们提供了负载均衡。「SpringCloud中的负载均衡算法可以自定义。」
  • 负载均衡简单分类:
    • 将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选出一个合适的服务器。
    • 「Ribbon属于进程式LB」,它只是一个类库,集成于消费方进程,消费方通过它获取到服务提供方的地址。
    • 即在服务的消费方与提供方之间使用独立的LB设施,如Nignx,由该设施负责把访问请求通过某种策略转发至服务的提供方。
    • 「集中式负载均衡」(LB)
    • 「进程式负载均衡」(LB)

(二)服务消费方简单集成Ribbon

此时服务消费方向Eureka集群获取到服务注册的列表,这时候只有一个服务提供者。

  1. 导入Ribbon的相关依赖
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-ribbon -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-ribbon</artifactId>
    <version>1.4.6.RELEASE</version>
</dependency>
      <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-eureka -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>


  • 需要注意的是要选择Spring-Cloud-stater的依赖
  • 由于需要从Eureka注册中心中获取有关服务信息,所以需要Eureka的依赖。(需要注意的是与Eureka服务中心不一样的依赖,这是配置Eureka的客户端,此处依赖与服务提供者的依赖一致。)
  1. 编写服务消费方的配置
# Eureka配置
eureka:
  client:
    register-with-eureka: false # 不向Eureka中注册自己
    service-url:
      defaultZone: http://eureka7001.cn:7001/eureka,http://eureka7002.cn:7002/eureka, http://eureka7003.cn:7003/eureka

  • 首先是选择不向Eureka中注册自己,因为此时是消费方,所以不需要向注册中心注册自己,只需要从注册中心获取服务信息即可。
  • 由于需要在注册中心获取服务方的信息,所以需要配置注册中心的所有可用的集群地址。
  1. 配置负载均衡的类
@Configuration
public class BeanConfig {
    /**添加负载均衡实现
     * */


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

}

添加注解@LoadBalanced,表明对restful请求模板对象实现了Ribbon支持(负载均衡支持)

  1. 修改控制层域名信息
    /**
     * 由于需要从远程的服务提供者获取相应的资源,所以需要设置远程提供者资源请求的固定前缀
     *private static final String REST_URL_PREFIX = "http://localhost:8001";
     *由于加入了集群,所以需要写入服务提供者的服务名
     * */

    private static final String REST_URL_PREFIX="http://SPRINGCLOUD-PROVIDER-DEPT";

这里由于需要负载均衡的支持,需要修改原先指定的ip地址为服务提供者的服务名(application.name),客户端设置的defaultZone的所有注册中心地址可以供客户端选择,客户端根据负载均衡的算法选择合适的注册中心进行访问。「即:原先的ip地址设置表示客户端(消费方)直接通过ip地址访问服务提供者,现在加入了Ribbon的负载均衡机制,通过选择合适的注册中心进行访问,不直接访问服务提供者。」

  1. 编写主启动类
@SpringBootApplication
@EnableEurekaClient
public class DeptConsumer {
    public static void main(String[] args) {
        SpringApplication.run(DeptConsumer.class,args);
    }
}

@EnableEurekaClient注解表明这个应用程序相对于Eureka注册中心来说,是一个客户端。

  1. 测试
  • 开启2个注册中心服务
  • 开启服务提供者服务
  • 开启服务消费方的服务,访问http://llocalhost:8080/getList即可访问到所有的信息

综上所述,Ribbon与Eureka的整合,客户端可以直接访问,不需要关心服务提供者的ip地址与端口号。

(三)Ribbon实现负载均衡

要实现负载均衡的效果,「必须要提供多个服务提供者,」(二)中只是集成了单个的消费提供者,并没有实际上的负载均衡。

  1. 创建多个数据库

分别是DB_02 ,DB_03

CREATE DATABASE DB_02;
USE DB_02;
CREATE TABLE dept(
    deptno bigint NOT NULL PRIMARY KEY AUTO_INCREMENT,
    dname VARCHAR(60NOT NULL ,
    db_source VARCHAR(60)
);
ALTER TABLE dept COMMENT ='部门表';
-- 使用数据库函数,读取数据库
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());


CREATE DATABASE DB_03;
USE DB_03;
CREATE TABLE dept(
    deptno bigint NOT NULL PRIMARY KEY AUTO_INCREMENT,
    dname VARCHAR(60NOT NULL ,
    db_source VARCHAR(60)
);
ALTER TABLE dept COMMENT ='部门表';
-- 使用数据库函数,读取数据库
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());
  1. 创建多个服务的提供者

分别创建SpringCloud-provider-8002和SpringCloud-provider-8003两个提供者

需要注意的是:

  • 「三个服务的服务名称必须要保持一致,即spring.application.name的值必须要保持一致,因为最终是通过服务名去访问对应的服务。」
  • 「并且需要修改相应的数据库配置,因为三个服务提供者的数据库是不一样的。」
  • 「并且需要修改相应的端口号,分别为8001、8002、8003」
  • 「对应的xxxMapper.xml文件需要进行相应的修改,改成」
  1. 测试服务
  • 首先启动Eureka注册中心服务(本例子中只启动了7001端口的注册中心服务)

  • 接着启动三个服务提供者,访问http://eureka7001.cn:7001

  • 接着启动服务消费者,访问http://localhost:8080/getList即可查看所有数据库的信息

虽然访问的是同一个请求地址,但是数据却来源不同的数据库,利用Ribbon实现了负载均衡。「默认采用的是轮询的算法」

(四)Ribbon自定义算法实现

由于Ribbon默认的算法实现是采用轮询,但是也可以自定义Ribbon的算法实现。

由于在服务消费者中实现Ribbon负载均衡的核心在于@LoadBalanced,它提供了我们Ribbon的默认算法实现。但其实还存在着一个IRule接口,它定义了一些负载均衡算法的实现规则。

「IRule接口源码」

public interface IRule {
    Server choose(Object var1);//选择对应的服务

    void setLoadBalancer(ILoadBalancer var1);//设置负载均衡的算法实现

    ILoadBalancer getLoadBalancer()//获取负载均衡的实现
}

同样的该接口下有很多的实现类:如下图所示

  • AvailabilityFilteringRule类表示会先过滤掉崩溃或者访问故障的服务,「对剩下的服务进行轮询」
  • RoundRobinRule类表示对所有的服务进行论询,默认实现
  • RandomRule类表示对所有的服务进行随机访问。
  • RetryRule表示会先按照论询获取服务,如果服务获取失败,则会在指定的时间内进行重试。
  • BestAvailableRule会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务;
  • ClientConfigEnabledRoundRobinRule 只是包装了RoundRobinRule 并没有实际的价值,并不会直接使用
  • WeightedResponseTimeRule表示根据平均响应时间计算所有服务的权重,响应时间越快的服务权重越大被选中的概率越大。「刚启动时如果统计信息不足,则使用RoundRobinRule(轮询)策略」,等统计信息足够,会切换到WeightedResponseTimeRule;
  • ZoneAvoidanceRule表示复合判断Server所在区域的性能和Server的可用性选择服务器,在没有Zone的情况下是类似轮询的算法;

因此要自定义自己的算法实现,可以编写一个自定义类,继承 AbstractLoadBalancerRule类,并重写相应的server的选择方法即可。

  1. 首先由于自定义的算法实现是自己写的组件,因此需要与主启动类在不同的目录层级(防止被主启动类扫描到,因为如果扫描到,就会被认为是该项目中的业务类)。下图选择在主启动类的上一层级编写组件。
  1. 修改主启动类
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "SPRINGCLOUD-PROVIDER-DEPT",configuration = MyRule.class)
public class DeptConsumer 
{
    public static void main(String[] args) {
        SpringApplication.run(DeptConsumer.class,args);
    }
}

添加注解@RibbonClient,该注解源码如下:

@Configuration
@Import({RibbonClientConfigurationRegistrar.class})
@Target(
{ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RibbonClient {
    String value() default "";

    String name() default "";

    Class<?>[] configuration() default {};
}

可以看到这是一个运行时注解,并且只能用于类,同时这是也是一个配置接口。它可以存入一些name和value以及配置类的数组。Class<?>[]

  • 本例子中,name指定了服务的名称,即需要对哪些服务进行相关的操作。
  • configuration指定了服务处理的相关配置,由于组件的配置不与主启动类在同一层级,因此主启动类启动的时候也就扫描不到服务处理的有关配置,此处指定了服务处理的配置。即在主启动类启动伊始,就预先加载服务处理的有关配置。
  1. 编写算法实现
public class MyRandomRule extends AbstractLoadBalancerRule {
    /**
     *表示服务被调用的次数
     * 当服务调用次数达到五次,则使用下一个服务
     */

    private  int total=0;
    /**
     * 表示当前被调用的服务,一共有3个,当超过数值超过3的时候,就应当被重置为0
     */

    private  int currentIndex=0;

        @SuppressWarnings({"RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE"})
        public Server choose(ILoadBalancer lb, Object key) {
            if (lb == null) {
                //如果负载均衡的对象为空,则返回为空
                return null;
            } else {
                Server server = null;

                while(server == null) {
                    if (Thread.interrupted()) {
                        return null;
                    }

                    //获取所有的可用的服务列表
                    List<Server> upList = lb.getReachableServers();
                    //获取所有的服务
                    List<Server> allList = lb.getAllServers();
                    //所有服务的个数
                    int serverCount = allList.size();
                    if (serverCount == 0) {
                        return null;
                    }

                    //// 生成区间随机数
                    //int index = this.chooseRandomInt(serverCount);
                    ////从可用服务数组中取得指定下表的服务
                    //server = (Server)upList.get(index);
                    if (total<5){
                        //此处表示如果当前的服务调用次数小于5,则获取当前服务
                         server = upList.get(currentIndex);
                         //获取完成后,调用次数自增
                        total++;
                    }else {
                        //如果调用次数超过五次,则需要将调用次数重置为0
                        total=0;
                        //同时服务编号自增1
                        currentIndex++;
                        //并且如果当前服务的编号大于了可用服务的个数,说明此时可用服务已经被遍历过一遍了。
                        if (currentIndex>=upList.size()){
                            //将要被调用的服务编号重置为0。
                            currentIndex=0;
                        }
                        server=upList.get(currentIndex);
                    }

                    if (server == null) {
                        Thread.yield();
                    } else {
                        if (server.isAlive()) {
                            return server;
                        }

                        server = null;
                        Thread.yield();
                    }
                }

                return server;
            }
        }

        protected int chooseRandomInt(int serverCount) {
            return ThreadLocalRandom.current().nextInt(serverCount);
        }

        @Override
        public Server choose(Object key) {
            return this.choose(this.getLoadBalancer(), key);
        }

        @Override
        public void initWithNiwsConfig(IClientConfig clientConfig) {
        }


}

此处的算法表示对于每个服务,如果访问超过五次,则将访问次数重置为0,并开始访问下一个可用服务。当前的可用服务编号(从0开始计数)超过或等于当前可用服务总数,则将可用服务编号重置为0.继续进行遍历。

  1. 对算法实现类交由容器管理
@Configuration
public class MyRule {

    @Bean
    public IRule getRule(){
        return new MyRandomRule();
    }

}
  • 使用@Configuration注解,表示这是一个配置类,可实现自定义的配置。
  • @Bean将自己实现的自定义类加入到Spring的容器中,交由容器托管。

六、Feign负载均衡

(一)Feign简介

1. 简介

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

「它的使用方法就是定义一个接口,然后在上面添加注解」,同时也支持JAX-RS标准的注解。Feign也支持可插拔式的编码器和解码器。SpringCloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。「Feign可以与Eureka和Ribbon组合使用以支持负载均衡。」

并且对于Ribbon来说,主要是使用微服务名称来调用指定的微服务(如SPRINGCLOUD-PROVIDER-DEPT),但对于Feign来说,主要是基于「接口和注解」,这也符合面向接口编程的编程习惯。

2. Feign的作用

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

3. Feign与Ribbon的关系

Feign集成了Ribbon、RestTemplate实现了负载均衡的执行Http调用,只不过对原有的方式(Ribbon+RestTemplate)进行了封装,开发者不必手动使用RestTemplate调服务,而是定义一个接口,在这个接口中标注一个注解即可完成服务调用,这样更加符合面向接口编程的宗旨,简化了开发。

如下图所示

(二)Feign的使用

  1. 新建一个项目,使用Feign来构建另一个服务消费者,module名称为SpringCloud-consumer-feign-8080
  2. 导入相应的依赖
   <!--实体类的有关依赖-->
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>SpringCloud-API</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <!--web支持-->
        <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>

   <!--添加eureka的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>
<!--        添加ribbon的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-ribbon</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>

<!--        添加feign的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>
  1. 建立对应的目录结构,并导入相应的配置文件
# 配置端口号
server:
  port: 8080

# 配置消费者的名字
spring:
  application:
    name: springcloud-consumer-dept
  # 配置热部署的端口
  devtools:
    livereload:
      port: 32961

# Eureka配置
eureka:
  client:
    register-with-eureka: false # 不向Eureka中注册自己
    service-url:
      defaultZone: http://eureka7001.cn:7001/eureka,http://eureka7002.cn:7002/eureka, http://eureka7003.cn:7003/eureka
  1. 编写主启动类,成为SpringBoot项目
@SpringBootApplication
@EnableEurekaClient // 注册成为Eureka的客户端
public class FeignDeptConsumer {
    public static void main(String[] args) {
        SpringApplication.run(FeignDeptConsumer.class,args);
    }
}
  1. 在API网关模块添加服务内容,并且加入相应的依赖

「这是因为消费者需要调用某个接口来访问服务,但是因为有很多消费者,所以将他们共有的所需要的接口提取出来放在了API模块中。」


<!--        添加feign的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>

@Component
@FeignClient(value = "SPRINGCLOUD-PROVIDER-DEPT")
public interface DeptClientService {

    /**
     * Query dept by id dept.
     *
     * @param id the id
     * @return the dept
     */

    @GetMapping("/get/{id}")
    Dept queryDeptById(@PathVariable("id") long id);

    /**
     * Query all dept list.
     *
     * @return the list
     */

    @GetMapping("/list")
    List<Dept> queryAllDept();

    /**
     * Add dept boolean.
     *
     * @param dept the dept
     * @return the boolean
     */

    @PostMapping("/add")
    boolean addDept(Dept dept);


}
  • 注解@FeignClient表示该服务可以被远程调用
  • value = "SPRINGCLOUD-PROVIDER-DEPT"表示指定了微服务的名称。
  • 并且对于接口中的方法可以直接指定访问的路径,类似于在contronller层中书写请求地址。
  • @Component表示将该类注册为一个组件。
  • 并且对于请求的书写尽量要与服务提供者的控制层请求保持一致,因为后续的服务消费者会利用远程调用本接口的方法来实现对服务的调用。
  1. 在SpringCloud-consumer-feign-8080模块中编写控制层程序
@RestController
public class DeptConsumerController {


    /***
     * 将deptClientService对象注入到当前项目中
     */

    @Resource
    private DeptClientService deptClientService;


    /**
     * 通过该方法远程获取指定id的Dept对象
     * 获取对象使用get方式
     *
     * @param id
     * @return
     */

    @RequestMapping("/getDept/{id}")
    public Dept getDeptById(@PathVariable("id") Long id) {
        return this.deptClientService.queryDeptById(id);
    }

    /**
     * 通过addDept方法提交一个内容,使用post方式请求
     * @param dept
     * @return
     */

    @RequestMapping("/addDept")
    public boolean addDept(Dept dept){
        return this.deptClientService.addDept(dept);
    }

    /**
     * 获取所有的内容,远程调用依旧是get方式
     * @return
     */

    @RequestMapping("/getList")
    public List<Dept> getDepts(){
        return this.deptClientService.queryAllDept();
    }

}
  • 此处使用@Resource注解直接将API网关中的接口DeptClientService 注入到当前模块之中。
  • 同样的在服务的消费方可以直接使用注入的DeptClientService 接口实现对象直接调用方法完成相应的业务。
  • 相比使用Ribbon的「通过服务名来调用相应的业务」来说,Feign的则更加符合面向接口编程。只注入相应的远程接口,就可以直接调用方法。
  1. 使feign生效
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.example.springcloud"})
public class FeignDeptConsumer {
    public static void main(String[] args) {
        SpringApplication.run(FeignDeptConsumer.class,args);
    }
}
  • @EnableEurekaClient接口表示注册为Eureka中心的客户端。
  • @EnableFeignClients(basePackages = {"com.example.springcloud"})表示开启Feign的相关服务,并且扫描API网关模块中服务的基本包名com.example.springcloud。因为在消费方中需要远程调用API网关模块中的有关方法,因此需要进行扫描操作。
  1. 测试访问
  • 首先开启Eureka注册中心的服务
  • 紧接着开启服务提供者,开启两个,分别占用8001端口与8002端口
  • 最后开启服务消费者Feign,访问:http://localhost:8080/getList即可查看所有的数据

七、Hystrix服务熔断与服务降级

(一)Hystrix的简介

1. 什么是Hystrix

Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免地调用失败,比如超时、异常等。Hystrix能够保证在一个依赖出现问题的时候,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。

“断路器”本身就是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),「向调用方返回一个服务预期的、可处理的备选响应(FailBack),而不是长时间的等待或者抛出异常,这样就可以保证服务调用方的线程不会被长时间不必要的占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。」

Hystrix源码托管在github上:https://github.com/Netflix/Hystrix

相关介绍:https://github.com/Netflix/Hystrix/wiki

2.Hystrix作用

  • 通过第三方客户端库访问(通常通过网络)依赖关系,保护和控制延迟和故障。
  • 停止复杂分布式系统中的级联故障。
  • 快速失败并迅速恢复。
  • 尽可能回退并优雅降级。
  • 实现近乎实时的监控、警报和操作控制。

3.服务雪崩

多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C调用其他的微服务,这就是所谓的「扇出」。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的「雪崩效应」

正常的调用如下所示:

对于高流量的应用来说,单一的后端依赖可能导致所有服务器上的所有资源在几秒钟内饱和。当许多后端系统之一存在故障时,它可以阻止整个用户请求:

比上述故障更糟糕的是,这些应用程序之间还可能导致服务之间的延迟增加、备份队列,线程和其他系统的资源紧张,导致整个系统发生级联故障,这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。

4. 服务熔断

熔断机制是「应对雪崩效应的一种微服务链路保护机制。」

当扇出链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,「进而熔断该节点的微服务调用,快速返回错误的响应信息」。当检测到该节点的微服务调用响应正常后恢复调用链路。在SpringCloud框架里的熔断机制通过Hystrix实现。Hystrix会监控微服务之间调用的状态。当失败的调用达到一定的阈值,缺省是5秒内20次调用失败就会启动熔断机制。熔断机制的注解是@HystrixCommand

(二)服务熔断机制实现

服务熔断:当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用。「往往是被动的。」

服务熔断主要在于服务提供者的相关操作。

  1. 新建一个服务提供方,为其添加熔断机制。新建项目名称为SpringCloud-provider-hystrix-8001,表示新建的添加熔断保护机制的服务提供者。
  2. 为新建项目添加相应的依赖
  <dependencies>
<!--        需要使用到API中的实体类,因为这是服务提供者,需要提供部门有关服务-->
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>SpringCloud-API</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

<!--        需要使用单元测试-->
        <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>

<!--        mybatis整合springboot的相关依赖-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>

<!--        导入springboot的相关测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-test</artifactId>
        </dependency>
<!--        导入springboot-web的依赖-->
        <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>

<!--        加入eureka依赖-->
        <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-eureka -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>

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

      <!--        导入Hystrix依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-hystrix</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>

    </dependencies>
  1. 为新建项目添加与之前的服务提供者一致的配置(application.yaml)
  2. 同样地为新项目建立各级目录以及配置
  1. 为该服务添加熔断机制

(1) 查看@HystrixCommand的源码

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface HystrixCommand {
    String groupKey() default "";

    String commandKey() default "";

    String threadPoolKey() default "";

    String fallbackMethod() default "";

    HystrixProperty[] commandProperties() default {};

    HystrixProperty[] threadPoolProperties() default {};

    Class<? extends Throwable>[] ignoreExceptions() default {};

    ObservableExecutionMode observableExecutionMode() default ObservableExecutionMode.EAGER;

    HystrixException[] raiseHystrixExceptions() default {};

    String defaultFallback() default "";
}

可以看出该注解主要在方法上使用并且是个运行时注解。比较重要的两个方法是fallbackMethod以及deafultFallback

(2)为服务提供者添加熔断机制

@RestController
@Accessors(chain = true)
public class DeptController {


    private DeptService deptService;
    @Autowired
    public DeptController(DeptService deptService) {
        this.deptService = deptService;
    }

    /**
     * 编写添加方法
     * @param dept
     * @return
     */

    @PostMapping("/add")
    @HystrixCommand(fallbackMethod = "addDeptHystrix")
    /**@RequestBody
     * 注解表示主要接收前端传递给后端的json字符串中的数据(即请求体中的数据,一般与post请求一起使用)
     * */

    public boolean addDept(@RequestBody Dept dept){
        return deptService.addDept(dept);
    }

    /**
     * 表示addDept的备用方法,如果addDept方法调用失败,则启动该方法。
     * @param dept
     * @return
     */

    public boolean addDeptHystrix(Dept dept){
        return false;
    }

    /**
     * 使用restful风格的写法获取单个部门
     * @param id
     * @return
     */

    @GetMapping("/get/{id}")
    @HystrixCommand(fallbackMethod = "hystrixGet")
    public Dept get(@PathVariable("id") Long id){
        return deptService.queryDeptById(id);
    }

    /**
     * 这是get方法的备用方法,当get失败后,则会调用该方法
     * @param id
     * @return
     */


    public Dept hystrixGet(@PathVariable("id") Long id){
        return new Dept().setDeptno(id)
                .setDname("没有对应id的相关信息(由Hystrix提供)")
                .setDb_source("We have none such database in  MySQL");
    }

    /**
     * 查询所有的部门信息
     * @return
     */

    @GetMapping("/list")
    @HystrixCommand(fallbackMethod = "queryAllHystrix")
    public List<Dept> queryAll(){
        return deptService.queryAllDept();
    }

    /**
     * queryAll的备用方法
     * @return
     */

    public List<Dept> queryALlHystrix(){
        List<Dept> list=new ArrayList<>();
        list.add(new Dept().setDeptno(1).setDname("空").setDb_source(""));
        list.add(new Dept().setDeptno(2).setDname("").setDb_source(""));
        
        return list; 
    }

  • @Accessors(chain = true)表示可以提供链式的写法,诸如new Dept().setDeptno(2).setDname("").setDb_source("")
  • @HystrixCommand(fallbackMethod = "xxx")表示被该注解标注的方法如果执行出现了失败。则将会启动该方法的备用方法,即xxx表示的是备用方法名。
  1. 编写主启动类
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@EnableCircuitBreaker
public class DeptProviderHystrix01 {
    public static void main(String[] args) {
        SpringApplication.run(DeptProviderHystrix01.class,args);
    }
}

@EnableCircuitBreaker注解表示添加对熔断机制的支持。

  1. 测试访问
  • 首先开启Eureka注册中心
  • 开启含有熔断机制的服务
  • 开启服务的消费者并访问 http://localhost:8080/getDept/2

如果访问到数据库中不存在的数据也不会报错,而是显示我们备用方法中的提供的信息。并不会报500的错误。

(三)服务降级机制实现

服务降级:服务降级是从整个系统的负荷情况出发和考虑的,对某些负荷会比较高的情况,为了预防某些功能(业务场景)出现负荷过载或者响应慢的情况,在其内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的fallback(退路)错误处理信息。

「往往是主动的。」

服务降级主要在于服务消费者的相关操作。

  1. 编写服务降级反馈信息

主要是针对API网关模块(因为很多的消费者接口都是共有的,需要提取出来放在一个模块中,再通过远程调用即可,不必去为每个消费者单独复制接口)

 @Component
public class DeptClientServiceFallBackFactory implements FallbackFactory {

    @Override
    public DeptClientService create(Throwable throwable) {
        //当前服务调用失败的时候,会反馈服务消费方一整个服务信息(DeptClientService)
        return new DeptClientService() {
            @Override
            public Dept queryDeptById(long id) {
                return new Dept().setDeptno(id)
                        .setDname(id+"没有对应的信息,并且该服务已经关闭了.....")
                        .setDb_source("MySQL中没有此信息");
            }

            @Override
            public List<Dept> queryAllDept() {
                return null;
                //开发中需要在方法中添加反馈信息
            }

            @Override
            public boolean addDept(Dept dept) {
                return false;
                //开发中需要在方法中添加反馈信息
            }
        };
    }
}

  • @Component表示将该类加入到组件中,交由容器托管
  • 重写FallbackFactory接口,表示该类将会在某一时刻对某些方法进行回调。
  • 重写create方法,返回的类型依据服务消费者访问的服务而定。(本例中服务消费者consummer8080主要调用API模块中定义的消费接口,即DeptClientService接口)
  • 该类表示如果真的需要进行「服务降级」操作,该类就会执行create方法,返回给服务调用方相应的提示信息(提示用户该服务进行了服务降级,可能现在已经无法访问了)。
  1. 配置消费服务接口
  • 首先查看FeignClient的源码,其中的Class<?> fallbackFactory() default void.class表示当服务发生降级的时候对消费者调用的很多服务方法进行回调的使用,Class<?> fallback() default void.class表示当服务发生降级的时候对消费者调用的单个服务方法进行回调的使用。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
    @AliasFor("name")
    String value() default "";

    /** @deprecated */
    @Deprecated
    String serviceId() default "";

    String contextId() default "";

    @AliasFor("value")
    String name() default "";

    String qualifier() default "";

    String url() default "";

    boolean decode404() default false;

    Class<?>[] configuration() default {};

    Class<?> fallback() default void.class;

    Class<?> fallbackFactory() default void.class;

    String path() default "";

    boolean primary() default true;
}

@Component
@FeignClient(value = "SPRINGCLOUD-PROVIDER-DEPT",fallbackFactory = DeptClientServiceFallBackFactory.class)
public interface DeptClientService 
{

    /**
     * Query dept by id dept.
     *
     * @param id the id
     * @return the dept
     */

    @GetMapping("/get/{id}")
    Dept queryDeptById(@PathVariable("id") long id);

    /**
     * Query all dept list.
     *
     * @return the list
     */

    @GetMapping("/list")
    List<Dept> queryAllDept();

    /**
     * Add dept boolean.
     *
     * @param dept the dept
     * @return the boolean
     */

    @PostMapping("/add")
    boolean addDept(Dept dept);


}

  • 该接口依旧是表示的是服务消费者远程调用的方法,并且使用Feign进行面向接口编程(类似于controller层设置,直接在接口方法上编写访问请求路径),之后服务消费方可以直接通过Restful风格请求直接调用服务提供者的接口方法(Feign采用这种方式),并且不需要提供相应的服务消费者的应用名(Ribbon采用这种形式)。
  • 但在注解@FeignClient进行修改,除了要指定该消费接口需要调用的服务提供者的名称,还需要知道当服务发生降级的时候对哪些类中的方法进行回调显示反馈信息。(也就是提示服务消费者,该服务进行了降级。)
  1. 在服务消费方开启服务降级
# 配置端口号
server:
  port: 8080

# 配置消费者的名字
spring:
  application:
    name: springcloud-consumer-dept
  # 配置热部署的端口
  devtools:
    livereload:
      port: 32961

# Eureka配置
eureka:
  client:
    register-with-eureka: false # 不向Eureka中注册自己
    service-url:
      defaultZone: http://eureka7001.cn:7001/eureka,http://eureka7002.cn:7002/eureka, http://eureka7003.cn:7003/eureka

# 服务降级开启
feign:
  hystrix:
    enabled: true

将feign.hystrix.enabled设置为true,表示开启「服务降级」(服务降级主要是在服务消费方设置)

  1. 测试
  • 首先开启Eureka注册中心
  • 接着开启服务提供者
  • 接着开启服务消费者,使用Feign的服务消费者,在8080端口。

但是出错了

这里大概的意思就是我们的DeptClientServiceFallBackFactorr类无法被实例化给@FeignClient中的服务提供者。

出现这个的主要原因:

@SpringBootApplication注解中自带了当前注解对所在类的所在包的扫描。也就是说当我们启动使用feign进行负载均衡的服务消费者的时候,当前启动类只会扫描到项目中的所有内容,但是定义服务降级的类和服务接口却不再当前项目中,所以当项目在启动的时候,会显示服务降级的类无法被实例化。主要还是因为无法被扫描到。

解决方案:

  • 添加服务降级的所在的包到启动项注解@SpringBootApplication注解中,并添加scanPackages指定到所在的包。
  • 与此同时,由于在启动项注解中添加了指定的扫描包,导致将原本扫描本模块的配置被覆盖掉了,所以此时还需要重新指定需要使用的包,例如controller

修改如下:

@SpringBootApplication(scanBasePackages = {"com.example.springcloud.service","com.consumer.springcloud.controller"})
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.example.springcloud"})
public class FeignDeptConsumer {
    public static void main(String[] args) {
        SpringApplication.run(FeignDeptConsumer.class,args);
    }
}

在注解中添加了扫描包的路径。

之后可以正常访问了。

  • 当服务没有被降级的时候,可以正常访问
  • 当人为关停服务提供者的时候再次访问,就会得到反馈信息

(四)实现服务监控

主要使用Hystrix实现流量监控,主要是针对「服务消费方」

  1. 新建一个项目,命名为springcloud-consumer-hystrix-dashboard-8080

  2. 为新项目添加相应的依赖

 <!--实体类的有关依赖-->
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>SpringCloud-API</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <!--web支持-->
        <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>

        <!--添加eureka的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>
        <!--        添加ribbon客户端工具依赖-->
        <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-ribbon -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-ribbon</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>

<!--        添加feign的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>
<!--        hystrix依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-hystrix</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>

<!--        hystrix监控依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>
  1. 添加相应的配置文件application.yaml
server:
  port: 9001
  1. 编写启动类
@SpringBootApplication
@EnableHystrixDashboard //开启hystrix监控注解
public class DeptConsumerDashBoard_9001 {
    public static void main(String[] args) {
        SpringApplication.run(DeptConsumerDashBoard_9001.class,args);
    }
}

@EnableHystrixDashboard 表示开启hystrix监控依赖,表示能够使用监控页面进行监控。

  1. 由于需要对服务的消费方的消费行为进行相应的监控,所以我们需要确保服务提供方提供了相应的监控依赖
<!--        完善相关的监控信息-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

<!--        hystrix依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-hystrix</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>

  1. 运行测试(此时尚未注册任何的服务),查看监控页面

访问http://localhost:9001/hystrix即可访问该页面

  1. 为了配合更好地监控服务,需要添加bean对象(在服务提供者的主启动类中添加)
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class DeptProvider01 {
    public static void main(String[] args) {
        SpringApplication.run(DeptProvider01.class,args);
    }

    @Bean
    public ServletRegistrationBean hystrixMetricsStreamServlet(){
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(new HystrixMetricsStreamServlet());
        registrationBean.addUrlMappings("/actuator/hystrix.stream");
        //增加路径映射,监控服务
        return registrationBean;
    }
}

  • @Bean表示将该ServletRegistrationBean对象交由容器托管。
  • ServletRegistrationBean 该类表示在SpringBoot中注册Servlet,用于拦截指定URL路径以及添加自定义操作。
  • ServletRegistrationBean 的创建需要以HystrixMetricsStreamServlet类流对象为参数
  • addUrlMappings("/actuator/hystrix.stream")表示添加映射的路径,表示当Hystrix的dashboard执行监控该路径的时候,能够进行稳定的检测服务。
  • 一般来说,获取注册的Servlet的方法是固定的。
  1. 测试实际监控

由于我们在服务提供者的主启动类中将 ServletRegistrationBean注入到IOC容器中,并且为其添加了监控的的映射路径,所以我们可以访问该路径(http://localhost:8001/actuator/hystrix.stream),就可以看到服务消费者的请求的JSON信息字符串。

但是这里存在一个问题:「存在空Ping」的现象,有两种原因:

  • 可能是服务消费者没有发出请求。
  • 也可能是配置出现了问题。

主要针对配置做一些说明

(1) 首先如果是使用Feign做负载均衡,一定要在服务消费方的主启动类上添加@EnableFeignClients注解,表明支持Feign做负载均衡。

(2)其次当使用了服务熔断或者服务接口的时候,需要在执行的方法(请求映射)中添加失败回调方法

@HystrixCommand(fallbackMethod = "xxxx")

(3)添加Hystrix的配置

management:
  endpoints:
    web:
      exposure:
        include: hystrix.stream  # *表示暴露所有的服务
  endpoint:
    hystrix:
      stream:
        enabled: true
        
# 服务降级开启(使用Feign,默认不使用Hystrix)
feign:
  hystrix:
    enabled: true        

重启所有的服务

  • 首先访问Eureka注册中心 http://eureka7001.cn:7001
  • 访问hystrix的监控初始页面http://localhost:9001/hystrix
  • 登陆服务消费者,访问http://localhost:8080/getDept/1获取信息
  • 为服务提供者注册监控信息(添加监控时间间隔、名称等)
  • 接着查看请求信息是否正常(正常会出现JSON数据)访问:http://localhost:8001/actuator/hystrix.stream查看

出现大片字符串。表示请求正常

  • 最后访问服务提供者的实时监控页面
  1. 监控页面

以下是监控页面的有关信息:

  • 「7色」

7中颜色分别代表不同程度的请求信息

  • 「1圈」

「实心圈」:有两种含义,通过颜色的变化代表了实例的健康程度

它的健康程度呈现绿色、黄色、橙色、红色递减,红色最危险。

该实心圈除了表示颜色的变化之外。它的大小也会根据实例的请求流量发生变化。流量越大,则该实心圈就越大。

通过实心圈的展示就可以在大量实例中快速发现「故障实例和高压力实例」

  • 「1线」

曲线:用来记录两分钟内流量的相对变化,可以通过它来「观察流量的的上升和下降趋势」

  • 整张图说明

八、Zuul路由网关

(一)API网关(GateWay)

1. API网关的概述

所谓的API网关其实就是指将所有API的调用统一接入API网关层,由网关层负责接入和输出。

什么时候需要网关呢?

「就是当我们的服务由单体应用架构变为微服务架构,这时候就需要网关的存在了。」

2. API网关的功能

一个API网关的基本功能包含了「统一接入、协议适配、流量管理与容错、以及安全防护」,这四大基本功能构成了网关的核心功能。

网关首要的功能是负责统一接入,然后将请求的协议转换成内部的接口协议,在调用的过程中还要有限流、降级、熔断等容错的方式来保护网关的整体稳定,

同时网关还要做到基本的安全防护(防刷控制),以及黑白名单(比如IP白名单)等基本安全措施。

(二)Zuul概述

1.什么是Zuul

Zuul包含对请求的路由和过滤两个最主要的功能:

其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础,而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础,Zuul与Eureka进行整合,将Zuul自身注册为Eureka服务治理下的应用。同时从Eureka中获得其他微服务的消息,即以后的访问微服务都是通过Zuul跳转后获得。

简单来说就是可以「隐藏后端项目的实际地址。」

「Zuul服务最终还是会注册进Eureka」

「提供:代理+路由+过滤三大功能」

2. Zuul的作用

  • 「身份验证和安全性」 - 确定每个资源的身份验证要求并拒绝不满足要求的请求。
  • 「洞察力和监控」 - 在边缘跟踪有意义的数据和统计数据,以便为我们提供准确的生产视图。
  • 「动态路由」 - 根据需要将请求动态路由到不同的后端集群。
  • 「压力测试」 - 逐渐增加集群的流量以衡量性能。
  • 「减载」 - 为每种类型的请求分配容量并丢弃超出限制的请求。
  • 「静态响应处理」 - 直接在边缘构建一些响应,而不是将它们转发到内部集群
  • 「多区域弹性」 - 跨 AWS 区域路由请求,以使我们的 ELB 使用多样化并使我们的优势更接近我们的成员

(三)Zuul路由实现

  1. 新建项目模块,命名为:SpringCloud-zuul-9981
  2. 添加相关的依赖
 <dependencies>
<!--        添加zuul的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zuul</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>
        <!--实体类的有关依赖-->
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>SpringCloud-API</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <!--web支持-->
        <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>

        <!--添加eureka的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>
        <!--        添加ribbon客户端工具依赖-->
        <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-ribbon -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-ribbon</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>

        <!--        添加feign的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>

        <!--        hystrix依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-hystrix</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>

        <!--        hystrix监控依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>
    </dependencies>
  1. 编写相应的配置文件application.yaml
# 设置服务端口
server:
  port: 9981

# 配置应用的名称
spring:
  application:
    name: SpringCloud-zuul

# 配置Eureka集群的地址
eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.cn:7001/eureka,http://eureka7002.cn:7002/eureka, http://eureka7003.cn:7003/eureka
  instance: #配置实例信息
    instance-id: Zuul9981.com # 实例id
    prefer-ip-address: true # 将实例的地址显示

# 配置一些项目信息
info:
  app.name: Zuul-9981
  company.name: GPNU
  1. 添加ip映射
sudo su
vim /etc/hosts

127.0.0.1  www.xxx.com
# 当访问该地址的时候,会被解析成为本机地址
  1. 编写Zuul的主启动类
@SpringBootApplication
@EnableZuulProxy
@EnableEurekaClient
public class ApplicationZuul9981 {
    public static void main(String[] args) {
        SpringApplication.run(ApplicationZuul9981.class,args);

    }
}

  • 其中@EnableZuulProxy表示开启Zuul服务代理
  • @EnableEurekaClient表示注册到注册中心。
  1. 简单测试
  • 开启Eureka注册中心7001端口
  • 开启服务提供者,在8001端口
  • 开启Zuul路由网关服务,在9981端口
  • 查看服务注册情况,可以看到服务提供者与Zuul同时被注册到注册中心中,访问:http://eureka7001.cn:7001/
  • 访问www.binlovejava.com/微服务名称/对应的请求,即可访问到服务提供者的相关服务(该域名可以在hosts中设置,实际对应的ip依旧是本机)
  1. 隐藏服务名访问

在使用网关来访问服务提供者的服务时,在请求中需要加入微服务的名称才能够访问到,但这种地址往往是不安全的,需要对其进行处理,使其访问时隐藏起来。

  • 配置application.yaml文件
# 配置网关
zuul:
  routes:
    mydept.mydept.serviceId: springcloud-provider-dept #原本微服务名称
    mydept.path: /dept/** # 现在更改的微服务名称
  ignored-services:  "*" #取消原来微服务名访问的权限  
  prefix: /bin #设置路由访问前缀

mydept.mydept.serviceId:表示原本的微服务id

mydept.path:自己自定义的访问名称,用于替代真实的微服务名称,即用此隐藏微服务名称。

ignored-services::设置为*表示本次项目中所有的微服务名称,表示本次项目中无法再使用任何微服务名称访问具体的微服务

  • 查看routes源码
  public void setRoutes(Map<String, ZuulProperties.ZuulRoute> routes) {
        this.routes = routes;
    }
//此处看出Routes的值主要是k-v键值对的形式

//以下是初始化方法
@PostConstruct
    public void init() {
        Iterator var1 = this.routes.entrySet().iterator();

        while(var1.hasNext()) {
            Entry<String, ZuulProperties.ZuulRoute> entry = (Entry)var1.next();
            ZuulProperties.ZuulRoute value = (ZuulProperties.ZuulRoute)entry.getValue();
            if (!StringUtils.hasText(value.getLocation())) {
                value.serviceId = (String)entry.getKey();
            }

            if (!StringUtils.hasText(value.getId())) {
                value.id = (String)entry.getKey();
            }

            if (!StringUtils.hasText(value.getPath())) {
                value.path = "/" + (String)entry.getKey() + "/**";
            }
        }

    }

设置完成之后,进行相关测试

  • 测试访问,当设置mydept.path: /dept/**之后,
  • 测试访问,当设置了ignored-services: "*" 之后访问,就无法使用原先的微服务名称来访问了
  • 测试访问,开启访问前缀prefix: /bin

此时使用原先的地址访问就失效了

在原先的隐藏字段之前,端口号之后加上前缀访问,即可正常访问。

综上Netfilx有五大组件:Eureka、Ribbon、Feign、Hystrix及Zuul

九、SpringCloud config分布式配置

(一)分布式配置概述

1. 分布式系统面临的配置文件问题

微服务意味着要将单体应用中的业务拆分成一个一个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务,由于每个服务都需要必要的配置信息才能运行,所以一套集中式的,动态的配置管理设施是必不可少的。SpringCloud提供了ConfigServer来解决这个问题。

2. SpringCloud config分布式配置中心

Spring Cloud Config 为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为「各个不同的微服务应用的所有环节提供了一个中心化的外部配置。」

Spring Cloud Config 分为「服务端与客户端」两部分:

  • 服务端也称为「分布式配置中心」,其是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息、加密、解密信息等访问接口。
  • 客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,在启动的时候从配置中心获取和加载配置信息。配置服务器默认从git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端来方便地管理和访问配置内容。

3. SpringCloud Config分布式配置中心作用

  • 集中管理配置文件
  • 不同环境、不同配置、动态化的配置更新、分环境部署,如:/dev、/test等。
  • 运行期间动态调整配置,不再需要为每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取配置自己的信息。
  • 当配置发生变动的时候,服务不需要重启,即可感知到配置的变化,并应用新的配置。
  • 将配置信息以REST接口的形式暴露

(二)SpringCloud Config实现

由于SpringCloud Config是一个C/S架构,所以需要分别配置服务端与客户端。

这里需要有个前提,就是在github的远程仓库有application.yaml配置文件。(后续的本地的config配置需要连接远程的仓库)

远程的application.yaml文件

spring:
  profiles:
    active: dev

---
spring:
  profiles: dev
  application:
     name: springcloud-config-dev

---
spring:
  profiles: test
  application:
     name: springcloud-config-test

注意:---不能少,并且 profiles与application处于同一级。

git add application.yaml
git commit -m "xxxx"
git push origin main
# 即可将配置文件推送到远程的github仓库

1. 配置SpringCloud Config的服务端

  • 新建一个项目,项目名为:SpringCloud-config-server-3366,表明这是这3366端口执行的配置中心服务
  • 导入相关的依赖
<!--        配置web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
<!--        导入Eureka配置-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>

<!--        导入config配置-->
        <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-config-server -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
            <version>3.1.1</version>
        </dependency>

  • 编写相应的配置application.yaml
server:
  port: 3366
spring:
  application:
    name: springcloud-config-server
    # 配置连接远程地址
  cloud:
    config:
      server:
        git:
          uri: https://github.com/xxxx/xxxx.git # 以http的方式连接
        default-label: main    

首先设置端口为3366,并配置用户名为springcloud-config-server,并配置该项目的远程连接地址,在github上。此外,uri选择「https」的形式,而不是git连接。

  • 编写主启动类
@SpringBootApplication
@EnableConfigServer
public class ConfigServer3366 {
    public static void main(String[] args) {
        SpringApplication.run(ConfigServer3366.class,args);
    }
}

注解@EnableConfigServer表示开启SpringCloud配置服务开启

  • 启动主启动类,并访问http://localhost:3366/application-dev.yaml

但是此时报错了:

表示连接超时。

再次访问:http://localhost:3366/application-dev.yaml

此外,由于我们在config-server的配置文件中设置了访问的默认标签main,所以访问:http://localhost:3366/main/application-dev.yaml也可以完成访问。

当访问不存在的配置文件之时,只会显示激活的配置文件

  • 至此,配置服务端完成配置。

2. 配置SpringCloud Config的客户端

  1. 首先编写一个客户端访问服务端的配置文件config-client.yaml
spring:
  profiles:
    active: dev

---
server:
  port: 8201
# 编写spring的有关配置
spring:
  profiles: dev
  application:
    name: springcloud-provider-dept


# 配置eureka
eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.cn:7001/eureka,http://eureka7002.cn:7002/eureka, http://eureka7003.cn:7003/eureka
---
server:
  port: 8202

# 编写spring的有关配置
spring:
  profiles: test
  application:
    name: springcloud-provider-dept


# 配置eureka
eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.cn:7001/eureka,http://eureka7002.cn:7002/eureka, http://eureka7003.cn:7003/eureka

此处表示对要使用远程服务端配置的客户端进行配置:

  • 激活dev环境的配置
  • 将开发环境的服务与测试环境的服务都注册到Eureka中。
  • 最后指定不同的端口
  1. 将编写好的文件推送到远程仓库
git add config-client.yaml
git commit -m "client config"
git push 
  1. 新建一个新的模块,命名为SpringCloud-config-client-3355
  2. 导入相应的依赖
<!--        web依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
<!--        监控信息-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
<!--        增加配置的依赖 2.1.1-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
            <version>2.1.1.RELEASE</version>
        </dependency>
  1. 编写config-client的相关配置

这里可以编写不同名称的配置文件,一个是bootstrap.yaml,一个是application.yaml,都能够被项目所识别。

  • bootstrap.yaml 表示是系统级别的配置,可以有效防止远程可能存在的配置冲突
# 系统级别的配置
spring:
  cloud:
    config:
      uri: http://localhost:3366 #需要读取配置服务的数据,因此uri为config-server的路径
      name: config-client #从git上读取刚刚写的客户端配置文件 不需要书写后缀
      label: main #添加标签,表示从哪个分支读取
      profile: dev #表示读取哪个环境的配置

这个配置文件的内容就是要读取配置服务的数据,也就是之前编写的配置服务端的数据(配置客户端需要从服务端处获取配置信息)。可以添加标签,表明从哪个git的分支上获取数据(本例是main主分支),profile表示要读取的是哪个环境下的配置(一般有dev、test、pro等环境)。name表示要读取的客户端配置文件的文件名。

config分布式配置中心(config-server)的任务就是远程管理所有的客户端的配置。因此尽管我们书写了客户端的配置文件config-client.yaml,依旧可以从服务端远程获取我们所需要的资源信息。

访问http://localhost:3366/application-dev.yaml,我们可以查看配置服务端自己所有的配置信息

此外,访问:http://localhost:3366/config-client-dev.yaml,也可以查看到指定的配置客户端的信息

  • application.yaml表示的是用户级别的配置
# 用户级别的配置
spring:
  application:
    name: springcloud-config-client
  1. 编写控制层组件,远程获取配置客户端的信息
@RestController
public class ConfigClientController {

    /**
     * 此处需要在控制层中远程获取配置客户端的信息,即Config-client的内容
     * 属性值的注入由于没有本地的配置文件,所以需要使用@Value远程获取,${....}
     * 完成资源的注入
     */


    @Value("${spring.application.name}")
    private String applicationName;

    @Value("${eureka.client.service-url.defaultZone}")
    private String eurekaServer;

    @Value("${server.port}")
    private String port;

    @RequestMapping("/config")
    public String getConfig(){
        return "applicationName:"+applicationName
                +",eurekaServer:"+eurekaServer
                +",port:"+port;
    }
}

  • @RestController表示访问到的信息以字符串的格式返回
  • 编写了三个属性,这是配置客户端需要从远程服务获取的信息,并且使用@Value注解来获取远程的配置信息(使用${xxx}来获取指定的信息内容)。
  • 最后通过/config的请求路径来获取远程的所有信息
  1. 编写主启动类
@SpringBootApplication
public class ConfigClient3355 {
    public static void main(String[] args) {       SpringApplication.run(ConfigClient3355.class,args);
    }
}
  1. 启动测试
  • 上图中标明了配置服务端的地址:http://localhost:3366
  • Tomcat初始化在8201端口(原先在配置客户端的配置文件中标明了8201是开发环境使用的端口号)
  • 最后的信息表明环境初始化成功
  • 「即使我们并没有在本地配置文件application.yaml文件中配置端口号(仅配置了应用名称),但在bootstrap.yaml文件中指定了需要读取的远程环境配置(profile:dev),因此最终我们即使没有指定端口,配置客户端也会远程读取配置服务端的信息(相当于是访问了http://localhost:3366/config-client-dev.yaml),这是因为bootstrap.yaml文件中设置了name属性,表示要读取的文件名(该文件位于github的远程仓库中)。」
  • 简单来说就是,配置客户端读取配置服务端的配置数据,配置服务端从远程仓库读取所需要的配置数据。
  1. 访问:http://localhost:8201/config可以获取到所读取的所有配置信息

此时表示配置完成。

(三)为Eureka和消费提供者配置远程配置

首先为Eureka和消费提供者分别创建远程配置文件

  • 为Eureka创建远程配置文件config-eureka.yaml
spring:
  profiles:
    active: dev

---
server:
  port: 7001

spring:
  profiles: dev
  application:
    name: springcloud-config-eureka

eureka:
  instance:
    hostname: eureka7001.cn
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://eureka7002.cn:7002/eureka, http://eureka7003.cn:7003/eureka
  server:
    enable-self-preservation: true

---
server:
  port: 7001

spring:
  profiles: test
  application:
    name: springcloud-config-eureka


eureka:
  instance:
    hostname: eureka7001.cn
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://eureka7002.cn:7002/eureka, http://eureka7003.cn:7003/eureka
  server:
    enable-self-preservation: true
  • 将其提交到远程仓库
git add config-eureka.yaml
git status
位于分支 main
您的分支与上游分支 'origin/main' 一致。

要提交的变更:
  (使用 "git reset HEAD <文件>..." 以取消暂存)

        新文件:   config-eureka.yaml

git commit -m "config-eureka"

git push origin main

  • 为服务提供者创建远程配置文件config-provider.yaml
spring:
  profiles:
    active: test

---
server:
  port: 8001

mybatis:
  type-aliases-package: com.example.springcloud.pojo
  config-location: classpath:/mybatis/mybatis-config.xml
  mapper-locations: classpath:/mybatis/mapper/*.xml

spring:
  profiles: dev
  application:
    name: springcloud-provider-config
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/DB_02?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true
    username: root
    password: 123456

eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.cn:7001/eureka,http://eureka7002.cn:7002/eureka, http://eureka7003.cn:7003/eureka
  instance:
    instance-id: java-spring-cloud01

info:
  app.name: demo for Spring Cloud
  company.name: GPNU
---
server:
  port: 8001

mybatis:
  type-aliases-package: com.example.springcloud.pojo
  config-location: classpath:/mybatis/mybatis-config.xml
  mapper-locations: classpath:/mybatis/mapper/*.xml

spring:
  profiles: test
  application:
    name: springcloud-provider-config
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/DB_03?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true
    username: root
    password: 123456

eureka:
  client:
    service-url:
      defaultZone: http://eureka7001.cn:7001/eureka,http://eureka7002.cn:7002/eureka, http://eureka7003.cn:7003/eureka
  instance:
    instance-id: java-spring-cloud01

info:
  app.name: demo for Spring Cloud
  company.name: GPNU
  • config-provider.yaml文件提交到远程配置仓库
git add config-provider.yaml
git status
git commit -m "config-provider"
git push origin main
  • 查看是否提交上去,登录github上查看

1. Eureka配置测试

  • 新建一个项目用于Eureka测试,命名为SpringCloud-config-eureka-7001
  • 将原先SpringCloud-eureka-7001的依赖以及相关的启动类等导入到新项目中。并且增加Config的包,用于部署本地配置的客户端
<!--        增加配置的依赖 2.1.1-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
            <version>2.1.1.RELEASE</version>
        </dependency>
  • 修改application.yaml文件(用户配置文件)
spring:
  application:
    name: SpringCloud-config-eureka
  • 新增bootstrap.yaml文件(系统配置文件)
spring:
  cloud:
    config:
      uri: http://localhost:3366
      name: config-eureka
      profile: dev
      label: main
  • 启动配置服务端(3366端口)访问http://localhost:3366/config-eureka-dev.yaml,即可看到是否可以正常访问
  • 启动Eureka-config项目,访问http://localhost:7001  或http://eureka7001.cn:7001即可访问注册中心页面
  • 至此Eureka启用远程配置中心成功

2. 为服务提供者配置远程配置

  • 新建一个服务提供者的项目,命名为:SpringCloud-config-provider-8001
  • 将原先的依赖以及相应的类导入
<!--        完善远程配置的信息-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
  • 新建一个application.yaml文件
spring:
  application:
    name: SpringCloud-config-provider
  • 新建一个bootstrap.yaml文件
spring:
  cloud:
    config:
      uri: http://localhost:3366
      name: config-provider
      label: main
      profile: dev
  • 启动主启动类,访问http://localhost:7001可以查看是否将服务注册进Eureka,再访问http://localhost:8001/list即可查看使用远程配置的信息查询本地数据库信息
  • 至此,服务提供者的远程配置就完成了。

往期内容回顾

你不可不知的语言---JAVA

Java基本程序设计结构——数据类型

变量+运算=?

字符串详解

输入输出与流程

数组与大数

类初识

自定义类与时间类

方法参数与对象构造

包、注释及JAR文件

继承及其子类

Object类及其方法

泛型数组列表及包装类

IO流概述

XML文档与解析

Java多线程之JUC

JDBC初识

一文带你了解Maven

MyBatis竟如此简单?

JavaWeb-教你如何实现交互

Apache Ant+Apache Maven—>Gradle

IOC与AOP—>Spring

SSM框架整合案例—以图书管理为例

SpringMVC概述

MyBatis-Plus利器

Spring-JDBC与事务

分布式与Dubbo

SpringBoot概述

枚举与反射浅析

<<< 左右滑动见更多 >>>


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存