springcloud是一套解决分布式应用框架的生态系统,是相关技术的集合解决方案。也就是说,springcloud是用来管理微服务的解决方案

包括服务注册与发现、统一配置管理、路由网关、事件总线、分布式会话、注册中心等组件组成 

本博客源码地址:https://github.com/leanmTree/spring-cloud-kuang

学习来源:狂神说JAVA

目录

 

一. springcloud前言

1. springcloud & springboot

2. dubbo  & springcloud

3. springcloud能做什么?

4. restFul调用

二. springcloud注册中心Eureka

1. eureka简介

2. 启动eureka服务

3. eureka的自我保护机制

4. eureka集群的搭建

5. zookeeper与eureka比较

三. springcloud调用通讯ribbon/feign

1. ribbon简介

1.1  负载均衡简介

2. 服务调用方使用ribbon

3. 自定义负载均衡ribbon

3.1 修改负载均衡策略(bean/配置文件)

3.2 自定义负载均衡策略

4. feign

四. springcloud熔断与网关

1. Hystrix简介

2. 服务熔断

3. 服务降级

4. 监控

5. 路由网关Zuul

五. 统一配置中心config


 

 

一. springcloud前言

针对目前的应用程序来说,当应用的服务器压力逐渐增大,可以通过部署集群的方式减缓访问压力,但是如果想进一步实现资源利用最大化,就需要将单一应用拆分为多个模块化的应用。模块之间通过通讯协同工作,共同负载应用。springcloud就为分布式应用的管理提供了一站式的解决方案。提供了高可用的服务架构。

1. springcloud & springboot

springboot是对spring的进一步封装,简化了应用开发和配置。但其开发的应用程序还是一个单体的应用。能够快速高效的开发一个一个的微服务

springcloud就是将这些springboot应用集合起来,组成一个大型应用的方案。并且给微服务之间提供了通讯、路由、熔断等方案。让微服务之间可以自由的通讯协同。

springboot是单独的微服务,springcloud是基于springboot实现的分布式架构治理解决方案。

2. dubbo  & springcloud

dubbo 是一个基于RPC通讯的远程调用服务包。早期由阿里巴巴开发,中间停更了5年,17年又被重启。是一个基于netty的异步调用分布式架构解决方案。

springcloud  是基于restFul网络通讯的同步阻塞服务调用框架。社区活跃度比较高。

相对于springcloud来说,dubbo就显得单调一点了。

因为如果只是用dubbo来搭建分布式系统,dubbo集成springboot很简单,就只需要导入依赖,写入配置文件,使用注解就能把一个服务注册为dubbo的bean来完成暴露。但是对于一个分布式系统来说,需要做的事情有很多。

例如,需要设计负载均衡、注册中心、需要进行服务熔断、需要做网关路由过滤等等

此时使用dubbo就需要我们自己选择与系统相对应的技术来配合搭建使用。灵活性很高,但想要搭建一个高性能高可用高并发的三高系统架构,与架构能力和技术能力会有很高的要求。

但是springcloud提供了这些分布式架构问题的所有解决方案。直接拿来使用即可。所以springcloud应用起来更加简单高效易于理解。

3. springcloud能做什么?

  • Distributed/versioned configuration(分布式/版本控制配置)
  • Service registration and discovery(服务注册与发现)
  • Routing(路由)

  • Service-to-service calls(服务于服务间的调用通讯)

  • Load balancing(负载均衡)

  • Circuit Breakers(断路器)

  • Global locks(全局锁)

  • Leadership election and cluster state(集群)

  • Distributed messaging(分布式消息)

springcloud中文API文档:https://www.springcloud.cc/spring-cloud-dalston.html#

4. restFul调用

区别于RPC的远程调用方式来说,通过restful的http调用会显得更加简单便捷。

两个微服务之间,不引入任何依赖和注入,直接通过http进行调用。例:A服务暴露了/list接口,则在B服务中,直接通过请求A服务的该接口,传入参数,获取返回值。完成两个微服务之间的相互调用。

springboot提供了restFul调用的模板类,restTemplate。直接向spring注册该bean,调用其中的API即可完成。

项目demo目录结构: 三个项目均被springcloud_pre作为父项目管理,common提供了pojo类,provider提供了对外访问接口,consumer通过restFul调用provider的接口

父工程的maven导入:

dependencyManagement是统一版本管理,也就是说,在子工程中,只需要指定dependency即可,不需要引入版本。目的是为了子工程的所需依赖版本是一致的。该标签一般只会在父工程中使用。

<?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>

    <!--spring-cloud父pom-->
    <groupId>com.lemon</groupId>
    <artifactId>parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>common</module>
        <module>provider-8001</module>
        <module>consumer80</module>
        <module>eureka-7001</module>
        <module>eureka-7002</module>
        <module>eureka-7003</module>
        <module>provider-8002</module>
        <module>provider-8003</module>
        <module>consumer-feign</module>
        <module>provider-hytrix-8004</module>
        <module>zuul</module>
        <module>config</module>
    </modules>

    <packaging>pom</packaging>

    <properties>
        <spring-cloud.version>Greenwich.SR2</spring-cloud.version>
        <spring-boot.version>2.1.6.RELEASE</spring-boot.version>
        <mysql.version>5.1.42</mysql.version>
        <druid.version>1.1.6</druid.version>
        <mybatis.version>2.1.3</mybatis.version>
        <junit.version>4.13</junit.version>
        <lombok.version>1.18.12</lombok.version>
        <log4j.versoon>1.2.16</log4j.versoon>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!--spring-cloud相关依赖-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--spring-boot依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--mysql-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>${druid.version}</version>
            </dependency>
           <!--springboot-mybatis启动器-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.version}</version>
            </dependency>
            <!--日志测试相关-->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>${junit.version}</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </dependency>
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>${log4j.versoon}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

 

二. springcloud注册中心Eureka

1. eureka简介

eureka是遵循CA原则设计的

通过eureka服务能够提供服务注册与发现的注册中心。提供了rest风格的服务调用。eureka服务就是我们启动的注册中心服务端

client客户端会连接eureka服务端,并向eureka发送心跳,保持连接。

2. 启动eureka服务

①创建一个maven工程,导入eureka的依赖包(可以去网上自行搜版本)

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

②编写eureka的配置文件

server:
  port: 7001

#配置当前实例的名称
eureka:
  instance:
    hostname: localhost
  client:
    register-with-eureka: false  #表示不向注册中心注册自己
    fetch-registry: false    #为false时,表示当前服务是注册中心,client设置为true
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/   #配置暴露的注册中心URL,其他客户端可以通过该URL连接注册中心

③配置启动类

启动类添加注解 @EnableEurekaServer 表示该启动类服务是一个eureka-server服务

package com.lemon.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
/**
 * @author hengtao.wu
 * @Date 2020/10/22 16:43
 **/
@SpringBootApplication
@EnableEurekaServer
public class Eureka7001Application {
    public static void main(String[] args) {
        SpringApplication.run(Eureka7001Application.class, args);
    }
}

④测试访问eureka的web监控页面:http://localhost:7001/

3. eureka的自我保护机制

当eureka在检测服务时,如果超过了心跳检测下限,就会启动自我保护机制,不会强制下线连接失败或者超时的服务,设计原则就是:宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。

Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%,Eureka Server 会将这些实例保护起来,让这些实例不会过期,但是在保护期内如果服务刚好这个服务提供者非正常下线了,此时服务消费者就会拿到一个无效的服务实例,此时会调用失败,
对于这个问题需要服务消费者端要有一些容错机制,如重试,断路器等。

可以使用eureka.server.enable-self-preservation=false(在注册中心服务eureka-server的yml文件中)来关闭保护机制(不推荐使用)

4. eureka集群的搭建

创建三个eureka项目,导入相同的依赖,启动文件。

修改配置文件后即可使用,eureka集群环境下,需要在每个eureka服务的配置文件中,声明其余注册中心的地址即可:

例,将三个eureka的hostname设置为:

eureka.7003.com、eureka.7003.com、eureka.7003.com

配置文件中声明其他的注册中心地址:

eureka:
  instance:
    hostname: eureka.7003.com
  client:
    register-with-eureka: false  #表示不向注册中心注册自己
    fetch-registry: false    #为false时,表示当前服务是注册中心
    service-url:
      #声明其余注册中心的地址
      defaultZone: http://eureka.7001.com:7001/eureka/,http://eureka.7002.com:7002/eureka/

此时,启动三个eureka后,即可在web页面上看到集群内容:

生产者消费者连接注册中心时,填写三个注册中心地址,往三个注册中心上注册自己即可。

生产者需要导入spring-cloud-starter-eureka的maven依赖。然后通过启动类添加@EanbleEurekaClient注解完成启动:

配置文件中声明注册中心地址:

#声明eureka注册中心地址
eureka:
  client:
    service-url:
      defaultZone: http://eureka.7001.com:7001/eureka/,http://eureka.7002.com:7002/eureka/,http://eureka.7003.com:7003/eureka/

启动服务提供者后,即可完成向三个注册中心注册服务

5. zookeeper与eureka比较

分布式系统的设置主要遵循的是C(一致性)、A(可用性)、P(分区容错性)原则,对于一个分布式系统来说,只能同时满足其中的两点。不能三者兼顾。

ZK的设计满足了CP原则,保证了一致性和分区容错性。也就是说对于ZK来说服务注册功能对一致性的要求是高于可用性的。对于ZK来说,注册中心的集群是存在master节点的。当master节点不可用时,slave节点会通过选举选出新的master节点,在这个过程中,整个ZK集群是出于不可用状态的。虽然保证了一致性,但可用性大大降低

eureka的设计满足了AP原则,保证了可用性和分区容错性。也就是说对于eureka来说,一致性是高于可用性的。eureka的集群每个节点都是平等的。当某个节点不可用时,其他节点仍然可以提供服务注册与发现的功能。所以说,只要有一台eureka节点还存在,就能够保证注册中心功能的正常使用。

本质上的区别就是设计原则不一样,从使用功能上来讲,都能够作为分布式系统的服务注册于发现的注册中心使用。

eureka能够很好地处理某些节点宕机不可用的网络故障问题,能够保证服务的可用性。不会像ZK一样出现网络故障后可能将会导致整个集群不可用。

三. springcloud调用通讯ribbon/feign

1. ribbon简介

spring cloud ribbon 是基于Netflix ribbon实现的一套 客户端负载均衡的工具

1.1  负载均衡简介

负载均衡(LoadBalance)就是将某个请求,通过算法决定将该请求发给哪个服务器进行处理。是用来针对分布式系统中分流限流,缓解服务器压力,实现性能最大化的一种方案

集中式LB:例如nginx,在发出请求后,通过一个中间代理,将请求进行分发

进程式LB:例如ribbon技术,在客户端发请求时,就获取一共有哪些服务,通过算法精准决定将本次请求发给哪个服务

2. 服务调用方使用ribbon

通过ribbon进行负载均衡请求时,rest请求会被转发到注册中心,从注册中心获取当前所有的服务,然后通过ribbon的算法决定请求将会发送给哪个服务提供者集群进行处理

注:如果消费者调用服务者的接口传递的参数是对象或者JSON,需要在服务端的参数加上注解@RequestBody,否则服务端接收到的参数可能是null

①导入maven依赖:spring-cloud-start-ribbon、spring-cloud-start-eureka

        <!--ribbon-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-ribbon</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>
        <!--eureka客户端依赖需要发现服务-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>

②修改配置文件,应用的rest请求将会从注册中心获取服务地址

eureka:
  client:
    # 针对调用方来说,就不需要向注册中心注册自己了
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka.7001.com:7001/eureka/,http://eureka.7002.com:7002/eureka/,http://eureka.7003.com:7003/eureka/

③启动类添加eureka客户端注解

@EnableEurekaClient

④restTemplate注册bean时,添加ribbon的负载均衡注解@LoadBalance

如果restTemplate添加了该注解,则进行rest请求时,就需要通过注册中心的服务名进行发送请求,因为加了该注解,表示rest请求时,是通过注册中心获取服务地址的,如果不写服务名,rest请求将无法识别服务地址。

此时默认是通过轮询的方式,如果服务提供者有多个,会依次分别将请求分发到各个服务器。

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

⑤进行请求:通过注册中心上的服务名,进行请求

 private static final String URL_PREFIX="http://provider/";

3. 自定义负载均衡ribbon

ribbon自带的负载均衡算法是实现了IRule接口的实现类,主要有以下几种

  • RoundRobinRule: 默认的轮询算法
    
  • AvailabilityFilteringRule:先过滤宕机(不可用)的服务,然后再进行轮询
  • RandomRule: 随机访问
  • RetryRule:重试算法,轮询时如果服务获取失败,则在指定时间内重试。

3.1 修改负载均衡策略(bean/配置文件)

①我们通过@LoadBalance注解加入到restTemplate中后,ribbon默认的负载均衡策略是轮询(RoundRobinRule),我们可以通过重新注入bean的方式,修改已经存在的负载均衡策略

@Configuration
public class BeanFactory {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
    #ribbon默认的IRule的实现类是轮询的bean,所以需要重新定义一个bean来替换负载均衡实现类
    @Bean
    public IRule my() {
        return new RandomRule();
    }
}

通过配置文件修改负载均衡策略

provider.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule

其中,provider是被调用服务(服务提供者的服务名)的名字。这样配置之后,调用provider这个服务的时候,负载均衡就使用的randomRule算法,后面指定负载均衡实现类的包路径即可(可以是ribbon提供的也可是自定义的)

这样配置,可以指定某个服务使用什么负载均衡算法

provider:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

 

3.2 自定义负载均衡策略

通过观察负载均衡IRule的实现类不难发现,所有的默认算法都是继承了AbstractLoadBalancerRule类,该类又是实现了IRule接口。所以我们要向自定义负载均衡算法,可以通过实现IRule接口或者继承AbstractLoadBalancerRule类即可,但是一般都是通过实现AbstractLoadBalancerRule类完成自定义负载均衡算法

注:自定义的负载均衡实现类需要放在启动类扫描不到的地方,因为如果放在springboot启动类扫描包下,就会导致所有的服务都会按照这个ribbon进行负载均衡,我们的目的是编写了自定义负载均衡,是给某一个服务使用的。而不是所有服务都使用

例:

我们在springboot启动类所在包或者子包中创建了如下文件:此时,启动时,所有的服务都会按照这个负载均衡进行调度

@Configuration
public class BeanFactory {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
    @Bean
    public IRule my() {
        return new MyRule();
    }
}

但我们的目的是,给某一个微服务指定一个负载均衡算法,则此时,就不能这么配置了,需要在springboot启动类上使用注解:

@RibbonClient(name = "provider", configuration = MyRule.class)

MyRule类是不在springboot扫描范围内的自定义负载均衡的类,此时就给provider这个服务,指定了这个自定负载均衡类。

如果要指定多个,可以使用:

@RibbonClients(value = {@RibbonClient(name = "user-service",configuration = NewRuleConfig.class)})

解决不被全局应用的方案有很多

  • 直接使用全局配置文件指定
  • 自定义注解类上不要加扫描注解,通过@RibbonClients去引入
  • 将自定义注解类放在扫描包以外

总结:

ribbon的负载均衡,源码提供了8中负载均衡IRule的实现类,主要有随机、轮询、权重、重试等算法。默认的server负载均衡都是轮询,如果要自定义负载均衡算法,可以通过实现IRule接口或者继承abstractLoadBalance类(本身abstractLoadBalance类就是实现了IRule接口的)编写自定义的负载均衡。

使用自定义负载均衡的方式一般有两种,一种就是通过yaml配置文件,给某个服务指定自定义负载均衡类

一种就是通过启动类上的注解,@RibbonClient(name='',configuration=),给某个服务指定某个自定义负载均衡类,但是要注意自定义的负载均衡类上如果使用了扫描注解,需要把他放在springboot启动类扫描不到的包中,否则springboot启动后,会注册该bean,就会导致所有的server共享该bean,覆盖其他server的负载均衡类。

4. feign

feign是springcloud实现服务之间相互调用的另一种方式,本质上还是ribbon访问服务进行通讯。所以说feign是对ribbon的一种包装功能。从而能够实现通过注解接口的方式。简化了ribbon的实现。

①编写feign接口

可以再common包中,编写feign提供功能的接口,导入spring-cloud-starter-feign依赖包,该项目只提供接口,不提供实现类,则是通过feign调用指定服务的接口来完成调用的,例,添加UserService的feign接口:

@FeignClient(value = "provider")  : 指明了当前接口是需要去访问provider这个服务的

该接口中的每个方法都指定了一个http请求,表示当服务调用这调用这个接口的方法时,它会被转发到指定服务的指定接口地址

写在接口上的URL地址,必须是provider这个服务对外提供的接口地址一致

方法的参数需要用@RequestBody、@RequestParam("id")修饰

package com.lemon.service;
@Component
@FeignClient(value = "provider")
public interface UserService {
    @PostMapping(value = "/provider/user/add")
    boolean add(@RequestBody UserPO userPO);
    @GetMapping(value = "/provider/user/getById")
    UserPO getById11(@RequestParam("id") Integer id);
    @GetMapping(value = "/provider/user/getAll")
    List<UserPO> getAll();
}

 

②编写消费端调用feign提供的接口

导入spring-cloud-starter-feign依赖包,编写controller,通过feign访问调用服务提供者的接口

通过注入的方式,注入上面编写的feign接口

然后直接调用方法,即可完成远程服务调用

@RestController
public class UserFeignController {
    @Autowired
    private UserService service;
    @RequestMapping("/feign/add")
    public Boolean add(UserPO userPO) {
        return service.add(userPO);
    }
    @RequestMapping("/feign/getById")
    public UserPO getById(Integer id) {
        UserPO byId = service.getById11(id);
        System.out.println("123");
        return byId;
    }
    @RequestMapping("/feign/getAll")
    public List<UserPO> getAll() {
        return service.getAll();
    }
}

当客户端提供了对外接口:/feign/getAll时,客户请求该接口,该接口会调用feign接口方法:getAll(),getAll()方法中指定了需要调用的服务名以及URL接口地址,远程请求了该服务后返回。

③feign消费者端启动类

消费者端通过feign进行服务调用的话,需要在启动类指定feign的service所在的包

basePackages 指定了service所在包(因为是两个不同的项目, 虽然maven导入了,但不能自动注入,所以启动时扫描这个项目的包,将其中的bean管理起来)
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients(basePackages = {"com.lemon.service"})
public class ConsumerFeignApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerFeignApplication.class, args);
    }
}

测试完成feign接口方法调用

④feign配置负载均衡

  • 通过注册IRule的实现类配置负载均衡
@Configuration
public class BeanFactory {
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
    @Bean
    public IRule my() {
        return new RandomRule();
    }
}
  • 通过yaml文件设置指定服务的负载均衡(可以配置自定义,常用),如下配置是指provider这个服务的负载均衡是randomRule
provider:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

总结:

springcloud分布式结构中,微服务之间的通讯采用ribbon或者feign都是可以的,但本质上讲,都是基于restFul进行http通讯的。

feign更加面向接口开发。但feign是在ribbon的基础上又加了一层通讯,可读性变高,但是性能相比ribbon却变低了

四. springcloud熔断与网关

1. Hystrix简介

在分布式系统应用中,存在许许多多的微服务。且对于某些业务来说,服务之间是相互依赖调用的。如果当依赖的某个服务不可用时,就会导致许许多多的问题,导致系统崩溃,所谓的雪崩效应

应对这种可能会出现的情况,我们需要对故障和延迟进行隔离和管理。从而应对避免可能出现的系统崩溃。

Hystrix是一个应用于处理分布式系统的延迟和容错的开源库,能够保证在一个依赖出现问题的情况下,不会导致级联故障、提高分布式系统的弹性。

应用在服务提供者 或者 服务消费者中,根据不同的业务需求进行服务熔断和降级操作。

应用:

  • 当某个服务超过了最大线程数,可以让剩余的请求进入hystrix方法。
  • 当连接超时后,可以进行降级
  • 当方法发生异常、服务不可用时,直接fallback
  • ......

代替品:

springCloudAlibaba-sentinel

 

hystrix也叫做“断路器”,相当于一种开关装置,当某个服务不可用时,通过断路器的故障监控,向调用方返回一个服务预期可处理的备份响应(fallback)。而不是长时间的等待或者返回一个不可处理的异常。减少资源的占用

可以应用于:

  • 服务降级
  • 服务熔断
  • 服务限流
  • 实时监控
  • ....

2. 服务熔断

当分布式系统中某个服务不可用时,会进行服务熔断处理,快速返回错误的响应信息。springcloud可以通过hystrix来实现熔断。熔断机制的注解是:@Hystrix

使用熔断demo:

demo项目结构截图:

启动eureka注册中心,启动provider-hystrix-8004作为服务提供者,启动consumer-feign作为服务调用者。模拟一个分布式的调用过程:

当feign服务调用者调用provider的服务时,服务接口不可用或者异常,此时通过hystrix进行熔断的demo

实现方式非常简单

①只需要在服务提供者的接口位置添加熔断注解和策略即可:

package com.lemon.provider.controller;

@RestController
public class UserController {
    @Autowired
    private UserService userService;

    @RequestMapping(value = "/provider/user/getById", method = RequestMethod.GET)
    //当消费者调用该接口时发生了异常,则转到调用备份API,进行快速响应
    @HystrixCommand(fallbackMethod = "getHystrix") 
    public UserPO getById(Integer id) {
        UserPO byId = userService.getById(id);
        if(null == byId) {
            throw new RuntimeException("null is ");
        }
        return byId;
    }

    public UserPO getHystrix(Integer id) {
        return new UserPO().setId(0).setName("这是空的----@Hystrix");
    }

}

②服务提供者启动类中添加hystrix的支持

@SpringBootApplication
@EnableEurekaClient       //该注解也是提供服务注册于发现(该注解只能注册到eureka注册中心中)
@EnableDiscoveryClient    //该注解提供服务注解与发现(该注解可以注册到任何注册中心)
@EnableCircuitBreaker       //添加对hystrix的支持
public class Provider8004Application {
    public static void main(String[] args) {
        SpringApplication.run(Provider8004Application.class, args);
    }
}

③测试访问当发生异常时,服务的调用就会跳转至fallback方法

3. 服务降级

当在某一时刻,某个服务的访问量特别高,而有的服务几乎没有访问。则此时需要将没有什么访问的服务进行降级,释放资源提供给访问量高的服务。

整体资源快不够用了,忍痛将某些服务先关掉,待度过难关,在开启回来

降级是在消费者端进行设置的,保证的就是在某些服务被关掉后,用户仍然能够访问,不过此时访问返回的是预设的缺省值

降级demo

项目结构图如上所示,此时提供feign的接口common端需要添加降级策略

①在提供给消费端接口的provider服务的common接口中,创建该接口的降级工厂实现类:

@Component
public class UserServiceFallbackFactory implements FallbackFactory {
    public UserService create(Throwable throwable) {
        return new UserService() {
            public boolean add(UserPO userPO) {
                return false;
            }

            public UserPO getById11(Integer id) {
                return new UserPO().setId(0).setName("provider服务已经停止运行,这是在common降级后的实现类提供的返回");
            }

            public List<UserPO> getAll() {
                return null;
            }
        };
    }
}

②在接口中添加降级的工厂类:当发现provider这个服务已经不可用后,直接返回上面的缺省返回

 **/
@Component
@FeignClient(value = "provider", fallbackFactory = UserServiceFallbackFactory.class)
public interface UserService {
    @PostMapping(value = "/provider/user/add")
    boolean add(@RequestBody UserPO userPO);
    @GetMapping(value = "/provider/user/getById")
    UserPO getById11(@RequestParam("id") Integer id);
    @GetMapping(value = "/provider/user/getAll")
    List<UserPO> getAll();
}

③客户端(服务消费者)端,要开启hystrix的降级设置

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

④启动provider、consumer、eureka进行测试,将provider停掉后,将会返回fallback的响应

如果发现consumer启动时报错No fallbackFactory instance,一般是由于无法扫描到common中得到fallback类导致的,所以在consumer启动时,添加扫描

@ComponentScan({"com.lemon.service","com.lemon.consumer"})   //springboot默认扫描当前包和子包,指定fallback类的包后,同时指定当前项目的包一起扫描进去。

4. 监控

springcloud的服务可以通过hystrix dashboard进行监控,此处不进行实例

改监控可以对服务的访问进行心跳检测,实时反映服务的健康程度、流量、压力等信息

5. 路由网关Zuul

Zuul包含了对请求的路由和过滤两个最主要的功能,能够对所有服务的访问路由进行统一过滤管理。

使用zuul实现所有服务统一路由访问和过滤demo:

①创建zuul项目,导入eureka、zuul、ribbon的maven依赖

②创建yml文件,配置端口,应用名,注册中心信息

server:
  port: 9200
spring:
  application:
    name: zuul
eureka:
  client:
    service-url:
      defaultZone: http://eureka.7001.com:7001/eureka/,http://eureka.7002.com:7002/eureka/,http://eureka.7003.com:7003/eureka/

③创建启动类,使用注解@EnableZuulProxy表名改启动类为网关代理

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

④启动注册中心、服务提供者、消费者、zuul项目,访问测试:

  • 原来服务提供者对外提供的接口:localhost:8001/provider/user/getById?id=16,正常访问,通过zuul的路由地址也可以访问:localhost:9200/provider/provider/user/getById?id=16,其中要加上服务的名字格式为:域名/服务名/地址
  • 原来feign消费者的接口:localhost/feign/getById?id=16,正常访问,通过zuul的路由地址同样可以反问:localhost:9200/consumer80/feign/getById?id=16 。同时,consumer80为注册到eureka上的服务名称

通过一个统一的应用,来访问管理所有服务的接口。实现了路由统一管理的功能。

⑤修改zuul路由的规则,给路由的服务名修改,不允许通过服务名访问等

zuul:
  routes:
    provider:   #给某个微服务起的别名,表示该微服务要拦截的服务ID以及替换的路由,可配置多个
      serviceId: provider  #原来的服务名
      path: /myzuul/**        #要替换的路由
    provider2:
      serviceId: provider2  #原来的服务名
      path: /myzuul/**        #要替换的路由
  ignored-services: "*"   #忽略服务,通过zuul访问时,不可以通过域名+服务名+URL的方式进行访问了,只能通过上面的代理路由访问 "*"表示所有的服务都忽略

⑥通过zuul、gateway网关实现鉴权验证

所谓的鉴权最常见的例子就是对用户的登录信息进行权限验证,在分布式系统中,如果每个服务都进过滤验证,很慢保证验证的一致性和准确性,并且是一个很繁琐的代码。

如果将用户校验这一块直接放在网关进行处理,就显得十分明智和方便。所以在网关中进行鉴权就显得很有必要了

用户的token信息通过JWT加密存放在请求中,加密信息中可以添加登录IP,防止token被盗用。请求进入网关后,进行验证,

实现鉴权的方式可以是在网关服务中新建类zuul继承ZuulFilter,gateway实现GlobalFilter, Ordered,实现鉴权功能,让所有服务在网关进行过滤,当然也可以设置直接放行的白名单。

demo 实例:

package com.lemon.zuul.filter;

import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * @author hengtao.wu
 * @Date 2020/10/30 10:37
 **/
@Component
public class ZuulFilter extends com.netflix.zuul.ZuulFilter {

    /**
     * 过滤器类型
     * @return  前置过滤器
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 过滤器顺序,越小越先执行
     */
    @Override
    public int filterOrder() {
        return 4;
    }

    /**
     * 过滤器是否生效
     * 此处表示/consumer80/feign/getById会进行过滤,其他的将会放行
     * 改方法直接return true,表示所有的请求都将会放行
     * @return true 拦击, false,不拦截
     */
    public boolean shouldFilter() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        if ("/consumer80/feign/getById".equalsIgnoreCase(request.getRequestURI())){
            return true;
        }
        return false;
    }

    /**
     *对于上面方法不放行的请求,将会执行该处的拦截逻辑方法
     */
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        String token  = request.getParameter("token");
        if(null == token) {
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
        }
        return null;
    }
}

此时,请求 /consumer80/feign/getById该地址将会被拦截,并且进行验证token是否为空,如果为空则会报错,拦截失败

用户鉴权一般流程:

* 对于一个业务系统的用户鉴权流程来说:
* 首先用户登录后,信息存放在token中。token存放在Redis,登录有效期为半小时。token通过http的head进行传输。
* 当客户端请求过来后,首先经过zuul网关,通过继承了zuulFilter进行过滤。通过userId,获取token是否存在Redis中,如果不存在,则提示未登录
* 如果存在并验证通过,则在网关层面放行,通过zuul的requestContent将head信息继续往下传递,传递给请求的服务端。
* 一般服务端会再次进行token验证,restControllerAdvice进行拦截验证。验证通过后将token中的用户信息解析出来,然后再进入业务权限的逻辑判断

 

五. 统一配置中心config

通过远程仓库完成分布式系统中服务的配置文件统一管理更新的作用,就是config的使命。

config分为服务端、客户端。服务端就是用来从远程仓库中拉去读取配置文件。

客户端从服务端获取配置文件并使用

配置中心config使用demo:provider通过配置中心,从git上获取配置文件

准备工作:

在github上创建一个公开的项目,创建如下配置文件:

①创建config配置中心服务项目

创建maven项目,并导入config的依赖包

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-server</artifactId>
            <version>2.1.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

②创建application.yml文件,配置github

  • uri:git项目地址
  • default-label:指定分支,默认是master
  • search-paths:为URI地址下的文件夹,如果在根路径就不需要写,此处demo的三个配置文件是在test文件夹下的
  • 如果是私有的项目,需要配置git的用户名密码
server:
  port: 3344

spring:
  application:
    name: config-server

  cloud:
    config:
      server:
        git:
          uri: https://github.com/leanmTree/spring-cloud-config
#          username: 535745426@qq.com
#          password: wht950808
          default-label: main
          search-paths: test/

③启动类添加注解

启动项目后,此时一个统一配置中心的服务端就创建好了。

当其他的服务需要获取配置文件时,只需要连接该服务端,从这里获取就好了。

启动后测试是否生效,可以通过localhost:3344/application/dev/main 看一下是够能顾查看到配置文件:

查看配置文件规则:

/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties

application:表示的应用(配置文件名)

profile:表示的环境(配置文件的命名要符合config规范,xxxxx-dev.yml)

label:表示分支

例如当前服务启动后,我要访问dev的配置文件:localhost:3344/application/dev/main、localhost:3344/application-dev.yml、localhost:3344/main/application-dev.yml

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

④创建客户端,通过配置中心获取配置文件

以provider项目为例,原来的application.yml文件如下:

#暴露端口
server:
  port: 8002

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: org.gjt.mm.mysql.Driver
    url: jdbc:mysql://124.70.157.112:3306/db02?useunicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: root
  application:
    name: provider

mybatis:
  type-aliases-package: com.lemon.pojo
  config-location: classpath:mybatis/mybatis-config.xml
  mapper-locations: classpath:mybatis/mapper/*.xml
#  configuration:
#    map-underscore-to-camel-case: true


#声明eureka注册中心地址
eureka:
  client:
    service-url:
      defaultZone: http://eureka.7001.com:7001/eureka/,http://eureka.7002.com:7002/eureka/,http://eureka.7003.com:7003/eureka/

现在将application.yml文件删除,通过读取git上的配置文件启动:

导入依赖包:

        <!--config-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
            <version>2.1.1.RELEASE</version>
        </dependency>

创建bootstrap.yml与application.yml文件(bootstrap.yml也是启动类配置文件,他是系统级的,启动优先读取改文件

bootstrap.yml:

连接上config服务端的服务器,并设置需要读取哪个分支、哪个环境的哪个配置文件

此处的name可以配置为git上存在的配置文件的任何name,但是路径是config server配置的search-paths路径下

spring:
  cloud:
    config:
      name: application #配置文件名
      label: main     #配置文件所在分支
      profile: test   #使用的配置文件环境
      uri: http://localhost:3344/   #配置中心的地址

application.yml:简单配置一下应用名称即可

spring:
  application:
    name: provider-8001

启动provider服务。此时测试发现,所有的配置文件都已经从git上获取到了

六. 消息总线BUS

可以将它理解为管理和传播所有分布式项目中的消息既可,其实本质是利用了MQ的广播机制在分布式的系统中传播消息,目前常用的有Kafka和RabbitMQ。利用bus的机制可以做很多的事情,其中配置中心客户端刷新就是典型的应用场景之一,我们用一张图来描述bus在配置中心使用的机制。

本质是利用MQ的广播,进行微服务之间的通讯

使用springcloud bus配置中心刷新demo:

①在配置中心服务config上导入bus、rabbitmq的依赖

        <!--spring-cloud-bus-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-bus</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>

②修改config服务的配置文件,添加rabbit的配置

  rabbitmq:
    host: localhost   #spring.rabbitmq.host

management:
  endpoints:
    web:
      exposure:
        include: bus-refresh   #配置MQ接受消息,当需要刷新配置时,给当前服务的MQ的改地址发送消息即可。localhost:3344/bus-refresh

③需要动态刷新配置文件的服务中导入依赖,修改配置文件

        <!--spring-cloud-bus-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-bus</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
            <version>2.2.1.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
  rabbitmq:
    host: localhost   #spring.rabbitmq.host

④测试:启动config客户端,在git上修改其他服务的配置文件,然后请求config服务发送MQ请求:localhost:3344/bus-refresh。发现配置文件动态生效了。

 

 

 

 

 

 

 

 

 

 

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐