Spring Cloud原理及应用

微服务架构概述

  • 2014年,Martin Fowler与James Lewis 共同提出了微服务的概念,定义了微服务是由以单一应用程序构成的小服务,自己拥有自己的行程与轻量化处理,服务依业务功能设计,以全自动的方式部署,与其他服务使用 HTTP API 通信。同时服务会使用最小的规模的集中管理能力,服务可以用不同的编程语言与数据库等组件实现
  • 特点:每个服务都比较简单,只关注于一个业务功能。
  • 优点:微服务架构方式是松耦合的,可以提供更高的灵活性。微服务可通过最佳及最合适的不同的编程语言与工具进行开发,能够做到有的放矢地解决针对性问题。每个微服务可由不同团队独立开发,互不影响,加快推出市场的速度。
  • 缺点:运维开销及成本增加,服务架构可能变成需要构建/测试/部署/运行数十个独立的服务,并可能需要支持多种语言和环境。代码重复:某些底层功能需要被多个服务所用,为了避免将“同步耦合引入到系统中”,有时需要向不同服务添加一些代码,这就会导致代码重复。

Spring Cloud

概述

  • 微服务是一种思想,最终需要技术架构去实现。它不是一种技术,而是一种架构思想
  • Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施和微服务的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。所以,Spring Cloud也并不是什么新的技术,依然是Spring的一员
  • 架构体系图:
    在这里插入图片描述
    通过ApI Gateway(网关)来实现数据的传递,还利用了分片和副本的技术
  • Spring Boot 和 Spring Cloud的关系
    Spring Boot可以离开SpringCloud独立使用开发项目,但是SpringCloud离不开SpringBoot,SpringBoot 是 Spring Cloud 开放的基础.
    Spring Boot可以整合其它技术完成微服务开发,只不过Spring Cloud是为了解决我们在微服务开发中遇到的一些共同难题,而提出的一套完整解决方案
    SpringCloud与SpringBoot版本对应:
    在这里插入图片描述

Spring Cloud全家桶

在这里插入图片描述

  • Spring Cloud Netflix OSS开源组件集成,包括Eureka、Hystrix、Ribbon、Feign、Zuul等核心组件。
    • Eureka:服务治理组件,包括服务端的注册中心和客户端的服务发现机制
    • Ribbon:负载均衡的服务调用组件,具有多种负载均衡调用策略
    • Hystrix:服务容错组件,实现了断路器模式,为依赖服务的出错和延迟提供了容错能力
    • Fengin:基于Ribbon和Hystrix的声明式服务调用组件
    • Zuul:API网关组件,对请求提供路由及过滤功能
    • Spring Cloud Bus:用于传播集群状态变化的消息总线,使用轻量级消息代理链接分布式系统中的节点,可以用来动态刷新集群中的服务配置。
    • Spring Cloud Consul:基于Hashicorp Consul的服务治理组件。
    • Spring Cloud Security:安全工具包,对Zuul代理中的负载均衡OAuth2客户端及登录认证进行支持。
    • Spring Cloud Sleuth:SpringCloud应用程序的分布式请求链路跟踪,支持使用Zipkin、HTrace和基于日志(例如ELK)的跟踪。
    • Spring Cloud Stream:轻量级事件驱动微服务框架,可以使用简单的声明式模型来发送及接收消息,主要实现为Apache Kafka及RabbitMQ。
    • Spring Cloud Zookeeper:基于Apache Zookeeper的服务治理组件。
    • Spring Cloud Gateway:API网关组件,对请求提供路由及过滤功能。
    • Spring Cloud OpenFeign:基于Ribbon和Hystrix的声明式服务调用组件,可以动态创建基于SpringMVC注解的接口实现用于服务调用,在SpringCloud 2.0中已经取代Feign成为了一等公民。

Eureka服务注册中心

HTTP REST远程调用

  • 新建父工程spring-cloud-test,编写通用依赖
    在创建时勾选两个依赖,dev tools热部署和spring web
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.example</groupId>
    <artifactId>spring-cloud-test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-cloud-test</name>
    <description>spring-cloud-test</description>
    <packaging>pom</packaging>

    <modules>
        <module>service-provider</module>
        <module>service-consumer</module>
    </modules>

    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • 新建service-provider模块,改写pom.xml,编写pojo、service、controller
    pom.xml:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.example</groupId>
        <artifactId>spring-cloud-test</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <artifactId>service-provider</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>service-provider</name>
    <description>service-provider</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

pojo创建实体类Product:

package com.provider.serviceprovider.pojo;

public class Product {
    private Integer id;
    private String pname;
    private double price;

    public Product() {
    }

    public Product(Integer id, String pname, double price) {
        this.id = id;
        this.pname = pname;
        this.price = price;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getPname() {
        return pname;
    }

    public void setPname(String pname) {
        this.pname = pname;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }
}

service:

//ProductService
package com.provider.serviceprovider.service;

import com.provider.serviceprovider.pojo.Product;

public interface ProductService {
    Product findById(Integer pid);
}

//ProductServiceImpl
package com.provider.serviceprovider.service.impl;

import com.provider.serviceprovider.pojo.Product;
import com.provider.serviceprovider.service.ProductService;
import org.springframework.stereotype.Service;

@Service
public class ProductServiceImpl implements ProductService {
    @Override
    public Product findById(Integer pid) {
        return new Product(pid,"华为",3999.00);
    }
}

controller:

package com.provider.serviceprovider.controller;

import com.provider.serviceprovider.pojo.Product;
import com.provider.serviceprovider.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/product/provider")
public class ProviderController {
    @Autowired
    ProductService productService;

    @GetMapping("/{pid}")
    public Product findProductByPid(@PathVariable Integer pid){
        return  productService.findById(pid);
    }
}

  • 新建service-consumer模块,改写pom.xml,编写pojo、config、controller
    pom.xml和provider模块的差不多
    pojo实体类是一样的
    config:
package com.example.serviceconsumer.config;

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

@Configuration
public class CustomConfig {

    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

controller:

package com.example.serviceconsumer.controller;

import com.example.serviceconsumer.pojo.Product;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/product/consumer")
public class ConsumerController {
    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/{pid}")
    public Product findByPid(@PathVariable Integer pid){
        return restTemplate.getForObject("http://localhost:8083/product/provider/"+pid, Product.class);
    }
}

  • 测试(单机+分布式调用测试),新建http文件测试
GET http://localhost:8082/product/consumer/102

###

Eureka概述

对于刚才的服务调用测试,我们发现如果,provider中的服务特别多,我们需要专门记录URL访问地址,而且如果某个服务的地址一旦改变我们还需要去手动改代码,复用性底,服务提供者和服务消费者耦合度高.那么有没有一种专门管理服务的组件呢,Eureka就是其中一款产品
Spring Cloud Eureka 是一个基于 REST 的服务,并且提供了基于java 的客户端组件,能够非常方便地将服务注册到 Spring Cloud Eureka 中进行统一管理

  • 服务治理是微服务架构中必不可少的一部分,阿里开源的 Dubbo 框架就是针对服务治理的。服务治理必须要有一个注册中心,除了用 Eureka 作为注册中心外,我们还可以使用 Consul、Etcd、Zookeeper 等来作为服务的注册中心。
    -
  • Eureka Server:提供服务注册和发现,多个Eureka Server之间会同步数据,做到状态一致(最终一致性)
  • Service Provider:服务提供方,将自身服务注册到Eureka,从而使服务消费方能够找到
  • Service Consumer:服务消费方,从Eureka获取注册服务列表,从而能够消费服务
  • 执行流程如下
    1.服务提供者将服务注册到eureka注册中心
    2.服务中心会记录服务的地址
    3.消费者从注册中心获取到服务列表
    4.根据需要选择相应的服务进行消费
    5.在服务续约(renew)中,Eureka客户端会每隔30秒发送一次心跳来进行服务续约。通过续约来告知Eureka服务器该客户端仍然存在,希望服务器不要剔除自己, 在默认的情况下,当Eureka客户端连续90秒没有像Eureka服务器发送服务续约的心跳(Heartbeat),Eureka服务器就会将该服务实例从服务注册列表中删除,即剔除该服务或者启用自我保护机制
    6.Eureka客户端在程序关闭时向Eureka服务器发送取消请求。发送请求后,该客户端实例信息将从服务器的实例注册列表中删除。

单机Eureka实现

  • Eureka实现中涉及到三个角色 Eureka Server,Provider,Consumer,提前分配好他们占用的端口号,这里假设分别为:8000,8001,8002
  • 创建父工程spring-cloud-eureka-demo,留一个Pom.xml就可以了
  • Eureka注册中心实现
    • 新建Eureka模块,修改pom.xml,修改application.yml ,创建时记得添加server依赖
server:
port: 8000
spring:
application:
name: eureka-server
eureka:
client:
register-with-eureka: false #是否将服务注册到注册中心 true代表注册,false代表
不注册 这里不自己注册自己,因此设置成false
fetch-registry: false #是否获取服务列表
service-url:
defaultZone: http://192.168.1.216:8000/eureka #注册中心的地址
# defaultZone: http://localhost:8000/eureka #注册中心的地址
  • 修改启动类:添加注解
@SpringBootApplication
@EnableEurekaServer //将该类作为注册中心
public class EurekaServerApplication {
	public static void main(String[] args) {
	SpringApplication.run(EurekaServerApplication.class, args);
	}
}
  • 启动访问localhost:8080(本地配置的访问路径)
    在这里插入图片描述
  • 服务提供者实现(provider)
    • 新建server-provider,修改pom.xml,修改application.yml
    • 创建的时候记得加一个server-client依赖
<!--也就是这个依赖-->
<dependencies>
<!--服务提供者引入的Eureka相关依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eurekaclient</artifactId>
</dependency>
</dependencies>

修改启动类(controller类):添加@EnableEurekaClient注解

  • 服务消费者实现
    • 新建server-consumer模块,修改pom.xml,也是要加server-client依赖
    • 修改application.yml(register-with-eureka: false #消费者将不注册到注册中心,根据需求设定)
    • 修改controller类,添加上面那个注解,而且:
public class ServiceConsumerController {
@Autowired
RestTemplate restTemplate;
/**
* 用于获取服务
*/
@Autowired
DiscoveryClient discoveryClient;
@GetMapping("/{pid}")
public Product findByPid(@PathVariable Integer pid) {
//如何调用到service-provider中的controller??
//我们需要利用RestTemplate基于(HTTP+REST)
//发出get请求,获取到的数据封装到Product.class对应的对象中
//return
restTemplate.getForObject("http://localhost:8080/provider/product/"+pid,
Product.class);
//return
restTemplate.getForObject("http://192.168.1.216:8080/provider/product/"+pid, Product.class);
//后期可能有eureka集群,所以不同的eureka注册中心上可能有相同服务
ServiceInstance serviceInstance =
discoveryClient.getInstances("service-provider").get(0);
String url = "http://"+serviceInstance.getHost() + ":" +
serviceInstance.getPort() + "/provider/product/" + pid;
return restTemplate.getForObject(url, Product.class);
}
}
  • 建http文件进行测试

Eureka集群搭建

  • 集群也就是两个及以上的注册中心互相注册,这里我就以两台主机为例来说明(192.168.2.140,192.168.2.130)
#140这台机器上的eureka-server的defaultZone应该配置成192.168.1.130地址
192.168.1.130
#130这台机器上的eureka-server的defaultZone应该配置成192.168.1.140地址
192.168.1.140
  • 修改eureka-server的application.yml,将它拆分方便后面的测试
    • application.yml
spring:
  profiles:
    active: cluster  # 指定application-cluster.yml配置文件的运行
# active: single,指定application-single.yml配置文件的运行
  • application-single.yml
server:
  port: 8000
spring:
  application:
	name: eureka-server
eureka:
  client:
    register-with-eureka: false #是否将服务注册到注册中心 true代表注册,false代
表不注册 这里不自己注册自己,因此设置成false
    fetch-registry: false #是否获取服务列表
    service-url:
      defaultZone: http://192.168.1.140:8000/eureka #注册中心的地址
  • application-cluster.yml
server:
  port: 8000
spring:
  application:
	name: eureka-server
eureka:
  client:
    register-with-eureka: false #是否将服务注册到注册中心 true代表注册,false代
表不注册 这里不自己注册自己,因此设置成false
#    fetch-registry: false #是否获取服务列表
    service-url:
      defaultZone: http://192.168.1.130:8000/eureka #注册中心的地址
  • 将defaultZone:http://192.168.1.140:8000/eureka 配置的eureka-server打成jar包上传到192.168.1.130
java -jar eureka-server-0.0.1-SNAPSHOT.jar
  • 将defaultZone:http://192.168.1.130:8000/eureka 配置的eureka-server打成jar包上传到192.168.1.140
java -jar  eureka-server-0.0.1-SNAPSHOT.jar
  • 分别访问两个地址

  • 为了保证高可用,为consumer和provider均配置两个注册中心

    • 修改provider的application.yml
spring:
  application:
    name: service-provider #应用标识
server:
  port: 8001
eureka:
  client:
  service-url:
  # defaultZone: http://192.168.1.216:8000/eureka
  defaultZone:
http://192.168.1.216:8000/eureka,http://192.168.1.181:8000/eureka
  • consumer的application.yml文件的url同样配置
  • 分别启动provider-server和consumer-server
  • 停掉192.168.2.140或192.168.2.140的注册中心,测试eureka集群是否可用

Eureka相关配置

  • provider配置
    • 优先使用配置的IP作为getHost()的值
spring:
  application:
    name: service-provider #应用标识
  server:
    port: 8001
eureka:
  client:
    service-url:
     # defaultZone: http://192.168.1.216:8000/eureka
      defaultZone:
http://192.168.1.216:8000/eureka,http://192.168.1.181:8000/eureka
  instance:
    ip-address: 127.0.0.1
    prefer-ip-address: true #true代表优先使用配置的ip,那么消费端通过getHost()
# 获取到的不再是主机名而是ip地址

在这里插入图片描述

  • 配置续约事件和心跳时间
    leaseExpirationDurationInSeconds: 表示 Eureka Server 在接收到上一个心跳之后等待下一个心跳的秒数(默认 90 秒),若不能在指定时间内收到心跳,则移除此实例并禁止此实例的流量或触发保护机暂时不移除实例
    leaseRenewalIntervalInSeconds:表示 Eureka Client 向 Eureka Server 发送心跳的频率(默认 30 秒)
    例如:默认情况下每隔30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳, EurekaServer就会认为该服务宕机,会定时从服务列表中移除
spring:
  application:
    name: service-provider #应用标识
  server:
    port: 8001
eureka:
  client:
    service-url:
     # defaultZone: http://192.168.1.216:8000/eureka
      defaultZone:
http://192.168.1.216:8000/eureka,http://192.168.1.181:8000/eureka
  instance:
    ip-address: 127.0.0.1
    prefer-ip-address: true #true代表优先使用配置的ip,那么消费端通过getHost()获取到的不再是主机名而是ip地址
	lease-renewal-interval-in-seconds: 2 # eureka client向eureka server发送心跳的频率,默认30s
	lease-expiration-duration-in-seconds: 2 # Eureka Server 在接收到上一个心跳之后等待下一个心跳的超时时间,默认90s
  • consumer配置
    eureka.client.registry-fetch-interval-seconds 设置获取服务列表的时间间隔(默认30s)
  • 注册中心配置
    • 服务下线
      当服务进行正常关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务中心接受到请求之后,将该服务置为下线状态。
      在这里插入图片描述
      服务剔除
      自我保护机制:我们可以通过eureka.server.enableSelfPreservation=false 关闭保护模式,那么一旦超时就会直接删除实例
      在这里插入图片描述
	server:
	enable-self-preservation: false #关闭注册中心的保护机制

Ribbon负载均衡

概述

在微服务架构中,很多服务都会部署多个(多个实例),其他服务去调用该服务的时候,如何保证负载均衡是个不得不去考虑的问题。负载均衡可以增加系统的可用性和扩展性,当我们使用RestTemplate来调用其他服务时,Ribbon可以很方便的实现负载均衡功能。

Ribbon是Netflix发布的云中间层服务开源项目,其主要功能是提供客户端实现负载均衡算法。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,Ribbon是一个客户端负载均衡器,我们可以在配置文件中Load Balancer后面的所有机器,Ribbon会自动的帮助你基于某种负载均衡策略(如简单轮询,随机连接等)去连接这些机器,我们也很容易使用Ribbon实现自定义的负载均衡算法。
在这里插入图片描述

多服务实例测试

  • 启动两个provider分别使用8081和8082端口
    在这里插入图片描述

  • consumer中使用负载均衡

    • 修改CustomConfig
@Configuration
public class CustomConfig {
	@Bean
	@LoadBalanced
	public RestTemplate restTemplate() {
	return new RestTemplate();
	}
}
  • 修改ServiceConsumerController
@RestController
@RequestMapping("consumer/product")
public class ServiceConsumerController {
@Autowired
RestTemplate restTemplate;
/**
* 用于获取服务
*/
@Autowired
DiscoveryClient discoveryClient;
	@GetMapping("/{pid}")
	public Product findByPid(@PathVariable Integer pid) {
	//使用负载均衡:Ribbon会根据负载均衡策略为consumer分配一个服务实例
	return restTemplate.getForObject("http://service-provider/provider/product/" + pid, Product.class);
	}
}
  • 修改service-provider打印出端口号
@RestController
@RequestMapping("provider/product")
public class ProductProvider {
	@Autowired
	ProductService productService;
	
	@Value("${server.port}")
	int port;
	
	@GetMapping("/{pid}")
	public Product findByPid(@PathVariable Integer pid) {
	System.out.println(port);
	return productService.findById(pid);
	}
}
  • 新建http测试
GET http://localhost:8002/consumer/product/200

根据控制台输出就可以直到访问的是哪个provider

Ribbon的配置

  • 全局配置
ribbon:
	ConnectTimeout: 1000 #服务请求连接超时时间(毫秒)
	ReadTimeout: 3000 #服务请求处理超时时间(毫秒)
	OkToRetryOnAllOperations: true #对超时请求启用重试机制
	MaxAutoRetriesNextServer: 1 #切换重试实例的最大个数
	MaxAutoRetries: 1 # 切换实例后重试最大次数
	NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #修改负载均衡算法
  • 单实例配置
# 在一开始这配置Ribbon实例名称:
service-provider:
  • 负载均衡策略(NFLoadBalancerRuleClassName)

Feign服务接口调用

  • feign是声明式的服务调用工具,我们只用创建一个接口并用注解的方式来配置 它,就可以实现对某个服务接口的调用,简化了直接使用RestTemplate来调用接口的开发量。Feign具备可插拔的注解支持,同时支持Feign注解及SpringMvc注解。当使用Feign时,Spring Cloud集成了Ribbon和Eureka以提供负载均衡的服务调用及基于Hystrix的服务容错保护功能。

Feign案例实现

  • 新建一个feign-service-consumer,修改pom.xml、application.yml、启动类、新建一个FeignService接口、新建controller
  • pom.xml:
<!--引入feign的依赖-->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<!--Feign需要用到eureka注册中心-->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-netflix-eurekaclient</artifactId>
</dependency>
  • application.yml:
server:
  port: 8004
spring:
  devtools:
    livereload:
      port: 35732
  application:
    name: feign-service-consumer #代表应用的标识

eureka:
  client:
    service-url:
            defaultZone: http://192.168.2.140:8000/eureka #设置eureka注册中心的地址
#      defaultZone: http://192.168.2.140:8000/eureka,http://192.168.2.130:8000/eureka #设置eureka注册中心集群
    register-with-eureka: false
#    registry-fetch-interval-seconds: 30 #消费者从注册中心抓取服务列表的时间间隔

#通用的日志级别
logging:
  level:
    com.bigdata.feignserviceconsumer.service: debug

  • 启动类:加一个@EnableFeignClients注解
  • 新建一个FeignService接口,指定服务提供者得application name
package com.bigdata.feignserviceconsumer.service;

import com.bigdata.feignserviceconsumer.pojo.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * 1.在@FeignClient注解中指定服务的id(application name)
 * 2.在接口定义的方法其实服务提供者中方法的抽象形式
 */
@FeignClient("service-provider")
public interface ProductFeignService {
    /**
     * 对ProductController中的方法进行绑定
     */
    @GetMapping("/provider/product/{pid}")
    public Product findByPid(@PathVariable Integer pid);
}

  • 新建controller,将FeignService接口导入使用
  • 测试

Feign+Ribbon负载均衡测试

feign已经自动集成了Ribbon,所以不需要额外得配置和注解

Feign日志打印

  • Feign得日志级别
    • NONE:默认,不显示任何日志;
    • BASIC:仅记录请求方法、URL、响应状态码及执行时间;
    • HEADERS:除了BASIC中定义的信息之外,还有请求头和响应头
    • FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。
  • 新建一个Feign配置类
package com.bigdata.feignserviceconsumer.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Feign日志级别
 */
@Configuration
public class FeignConfig {
    @Bean
    public Logger.Level level() {
        return Logger.Level.BASIC; //FULL的feign日志级别
    }
}

  • 配置feign日志,修改application.yml
#通用的日志级别
logging:
  level:
    com.bigdata.feignserviceconsumer.service: debug
  • Feign其他配置
feign:
  hystrix:
    enabled: true #在Feign中开启Hystrix
  compression:
    request:
      enabled: false #是否对请求进行GZIP压缩
      mime-types: text/xml,application/xml,application/json #指定压缩的请求数据类型
      min-request-size: 2048 #超过该大小的请求会被压缩
    response:
      enabled: false #是否对响应进行GZIP压缩

Hystrix熔断降级

微服务中的雪崩效应

随着业务的不断扩展以及复杂度不断提升,服务的数量也会随之增多,逻辑变得越来越复杂,一个服务的某个逻辑需要依赖多个其他服务才能完成。一但一个依赖不能提供服务很可能会产生雪崩效应,最后导致整个服务不可访问
通俗的说:就是当一个微服务出问题时,会导致一直停留、卡在那个服务,当越来越多的访问,在那个微服务上一直转圈圈,那就会导致堵塞,造成雪崩效应

Hystrix概述

Hystrix是一个由Netflix发起的用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性
Hystrix具备服务降级、服务熔断、线程隔离、请求缓存、请求合并及服务监控等强大功能

Hystrix降级

概述

熔断:如果某个目标服务调用慢或者有大量超时,此时,熔断该服务的调用,对于后续调用请求,不在继续调用目标服务,直接返回,快速释放资源,如果目标服务情况好转再恢复调用
相当于是保险丝,把保险丝烧了,然后保全全部电路的正常运行
在这里插入图片描述
服务降级:当某个服务熔断后,我们可以准备一个fallback的回调,返回一个自定义的值

RestTempalte+Hystrix实现

  • 修改service-provider,新建Result类
package com.bigdata.serviceprovider.pojo;

/*
* 统一返回JSON格式数据给前端
* */
public class Result<T> {
    private int code;
    private String msg;
    private T data;  //封装的对象

    public Result() {
    }

    public Result(int code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

  • 修改service-provider的controller
package com.bigdata.serviceprovider.controller;

import com.bigdata.serviceprovider.pojo.Product;
import com.bigdata.serviceprovider.pojo.Result;
import com.bigdata.serviceprovider.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/provider/product")
public class ProductController {
    @Autowired
    ProductService productService;

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

//    @GetMapping("/{pid}")
//    public Product findByPid(@PathVariable Integer pid) {
//        System.out.println(port);
//        return productService.findByPid(pid);
//    }
    @GetMapping("/{pid}")
    public Result<Product> findByPid(@PathVariable Integer pid) {
        return new Result<>(200,"success",productService.findByPid(pid));
    }
}

Hystrix降级有两种,一种是单独为一个方法指定的,一种是统一的降级处理

  • 单独指定:在controller方法上指定,例如
@GetMapping("/{pid}")
@HystrixCommand(fallbackMethod = "findByPidFallback")//降级方法
public Result<Product> findByPid(@PathVariable Integer pid) {
	Product product = restTemplate.getForObject("http://serviceprovider/provider/product/" + pid, Product.class);
	return new Result<>(200,"success",product);
}

public Result<Product> findByPidFallback(Integer pid) {
	return new Result<>(500,"findByPid降级信息",null);
}
  • 定义统一的降级方法
/*
如果同时定义了统一的降级和针对某个方法降级,那么一旦服务发生异常,会调用针对该方法的降级
统一降级的方法必须兼容所有**要降级的所有方法的返回值类型**,否则会抛出异常
*/

//controller中
@GetMapping("/{pid}")
//@HystrixCommand(fallbackMethod = "findByPidFallback")//单独指定降级方法
@HystrixCommand  //用统一的降级方法
public Result<Product> findByPid(@PathVariable Integer pid) {
	Product product = restTemplate.getForObject("http://serviceprovider/provider/product/" + pid, Product.class);
	return new Result<>(200,"success",product);
}

@GetMapping("/list")
@HystrixCommand
public Result findAll() {
	return restTemplate.getForObject("http://serviceprovider/provider/product/list", Result.class);
}

/**
* 通过用法需要兼容所有要降级的方法的返回值类型,前提是有一个共同的类Result
* @return
*/
public Result generalFallback() {
return new Result(501,"通用降级信息",null);
}
  • Feign+Hystrix实现
    如果是用Feign,直接在实现类中新建降级类就可以
    application.yml加的配置
#启用hystrix与feign的整合
feign:
hystrix:
enabled: true

实现类指定降级方法即可,不用再到controller中

@Component
public class ProductFeignFallBack implements ProductFeignService {
@Override
public Result findByPid(Integer pid) {
return new Result(500,"findByPid降级信息",null);
}
@Override
public Result findAll() {
return new Result(501,"findAll降级信息",null);
}
}

然后在controller中将相应的接口引入,调用方法即可

Hystrix监控面板

  • 概述
    Hystrix Dashboard主要用来实时监控Hystrix相关工程的各项指标信息。通过Hystrix Dashboard反馈的实时信息,可以帮助我们快速发现系统中存在的问题。
  • 新建hystrix-dashboard模块,添加依赖
<dependencies>
	<!--hystrix监控面板相关依赖-->
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-hystrixdashboard</artifactId>
	</dependency>
</dependencies>
  • 修改hystrix-dashboard模块配置文件指定访问端口
server:
  port: 9000
  
hystrix:
  dashboard:
    proxy-stream-allow-list: '*' #监控的主机地址,否则无法获取被监控服务的数据
  • 修改启动类,添加@EnableHystrixDashboard依赖
  • 修改hytrisx-service-consumer模块的依赖
    也就是说,要被hystrix监控事务微服务,都要添加该依赖
<dependencies>
	<!--需要加入该依赖才能被Hystrix监控-->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-actuator</artifactId>
	</dependency>
</dependencies>
  • 修改hystrix-service-consumer的配置文件
#对外暴露hystrix监控相关的端点,否则无法访问
management:
  endpoints:
    web:
      exposure:
        include: hystrix.stream #'*'代表暴露所有的端点
  • 启动service-provider,hystrix-service-consumer,hystrix-dashboard
    在这里插入图片描述
    在这里插入图片描述
    Hystrix Dashboard+turbine
  • 新建turbine-service模块,引入以下依赖
<dependencies>
<!--turbine依赖-->
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-turbine</artifactId>
	</dependency>
	<!--eureka依赖:需要收集注册中心中的服务-->
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-eurekaclient</artifactId>
	</dependency>
<dependencies>
  • 修改turbine-service的配置文件
server:
  port: 9001
spring:
  application:
    name: turbine-service #代表应用的标识
    
eureka:
  client:
    service-url:
      defaultZone:
http://192.168.1.216:8000/eureka,http://192.168.1.181:8000/eureka #设置eureka注册中心集群

turbine:
  app-config: hystrix-service-consumer,hystrix-feign-consumer #配置监控的微服务
  cluster-name-expression: new String("default")
  • 修改启动类,加@EnableTurbine注解
  • 修改hystrix-feign-consumer添加监控依赖依赖以及修改配置
management:
  endpoints:
    web:
      exposure:
        include: '*'
  • 访问http://localhost:9000/hystrix hystrix dashboard面板,然后在输入 Turbine收集信息的地址
    在这里插入图片描述
    在这里插入图片描述

Hystrix熔断

熔断原理图

在这里插入图片描述

  • 在要监控的模块中加如下配置
hystrix:
  command: #用于控制HystrixCommand的行为
    default:
      circuitBreaker: #用于控制HystrixCircuitBreaker的行为
      enabled: true #用于控制断路器是否跟踪健康状况以及熔断请求
      requestVolumeThreshold: 5 #熔断的最小请求数 默认值20
      sleepWindowInMilliseconds: 15000 #断路器跳闸后,在此设置值的时间的内,hystrix会拒绝新的请求,只有过了这个时间断路器才会允许访问服务
#,默认为5000(5秒)
      errorThresholdPercentage: 80 #服务的健康状况 = 请求失败数 / 请求总数>=80%触发熔断 默认50%
  • 最后访问监控模块的地址,看改变的状态

资源隔离

  • 线程池策略
  • 信号量策略
    当请求的服务网络开销比较大的时候,或者是请求比较耗时的时候,我们最好是使用线程隔离策略,这样的话,可以保证大量的容器(tomcat)线程可用,不会由于服务原因,一直处于阻塞或等待状态,快速失败返回。而当我们请求缓存这些服务的时候,我们可以使用信号量隔离策略,因为这类服务的返回通常会非常的快,不会占用容器线程太长时间,而且也减少了线程切换的一些开销,提高了缓存服务的效率。
  • 配置:
hystrix:
  command:
    default:
      execution:
        isolation:
          strategy: ExecutionIsolationStrategy.SEMAPHORE #信号量隔离
          # ExecutionIsolationStrategy.THREAD 线程池隔离

Spring Cloud Gateway

网关概述

API网关出现的原因是微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求。如果让客户端直接与各个微服务通信,会有以下的问题:

  1. 客户端会多次请求不同的微服务,增加了客户端的复杂性。
  2. 存在跨域请求,在一定场景下处理相对复杂。
  3. 认证复杂,每个服务都需要独立认证。
  4. 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施。
  5. 某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难。
    在这里插入图片描述

Spring Cloud Gateway

在这里插入图片描述
Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流
路由(route): 路由信息的组成:由一个ID,一个目的URL,一组断言工厂,一组Filter组成。如果路由断言
为真,说明请求URL和配置路由匹配。
断言(Predicate):Spring Cloud Gateway中的断言函数输入类型是Spring 5.0框架中的ServerWebExchange。Spring Cloud Gateway的断言函数允许开发者去定义匹配来自于Http Request中的任何信息比如请求头和参数。
过滤器(Filter):一个标准的Spring WebFilter。 Spring Cloud Gateway中的Filter分为两种类型的Filter,分别是Gateway Filter和Global Filter。过滤器Filter将会对请求和响应进行修改处理。

gateway的测试

  • 新建spring-cloud-gateway模块进行测试
  • 在之前的基础上要加一个gateway依赖:
    eureka依赖那肯定也是要加的,还有锁定版本的依赖
<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
  • 修改application.yml:
spring:
  application:
    name: spring-cloud-gateway
  cloud:
    gateway:
      routes: #给List<RouteDefinition>注入数据
        - id: service-consumer  #应用的标识
          uri: http://localhost:8082  #由网关路由到微服务的地址,也就是真实地址
          predicates:  #匹配规则
            - Path=/consumer/**  #可以写多个path

eureka:
  client:
    service-url:
      defaultZone: http://192.168.2.140:8000/eureka  #设置eureka(注册中心集群)
  • 在启动类上加一个EnableEurekaClient注解
  • 启动测试:
#### consumer/product/list匹配/consumer/**
#### 转发到 http://localhost:8002/consumer/product/list

GET http://localhost:8080/consumer/product/list

###

常用的predicates(断言)

spring:
  cloud:
    gateway:
      routes:
      - id: after_route
      uri: https://example.org
      predicates:
      - After=2017-01-20T17:42:47.789-07:00[America/Denver] #在指定的时间之后的请求会匹配该路由
spring:
  cloud:
    gateway:
      routes:
      - id: before_route
      uri: https://example.org
      predicates:
      - Before=2017-01-20T17:42:47.789-07:00[America/Denver] #在指定的时间之后的请求会匹配该路由
spring:
  cloud:
    gateway:
      routes:
      - id: between_route
      uri: https://example.org
      predicates:
      - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver] #在该时间段内的请求会匹配该路由
spring:
  cloud:
    gateway:
      routes:
      - id: hystrix-service-consumer #自定义的路由 ID,保持唯一
      uri: https:locahost:8005 # 路由到的服务地址
      predicates: #路由条件(匹配规则)
      - Path=/consumer/product/**
      - Cookie=name,/W*	  # /W*:匹配规则

使用Ribbon访问目标服务

  • 在pom.xml中添加eureka依赖就可以,因为eureka已经集成了Ribbon
  • application.yml:
spring:
  cloud:
    gateway:
      routes:
      - id: hystrix-service-consumer #自定义的路由 ID,保持唯一
      uri: lb:https:locahost:8005 #load-balancing缩写,底层会使用ribbon做负载均衡
      # 路由到的服务地址
      predicates: #路由条件(匹配规则)
      - Path=/consumer/product/**
      - Cookie=name,/W*	  # /W*:匹配规则

过滤器

概述
当客户端向 Spring Cloud Gateway 发出请求。如果 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。 过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑
从拦截范围可以分为:
GatewayFilter(局部过滤器):将应用到单个路由或者一组路由上
GlobalFilter(全局过滤器):应用到所有路由上
内置GatewayFilter使用
Spring CLoud官网

在这里插入图片描述
在这里插入图片描述

  • 测试AddResponseHeader
spring:
  application:
    name: spring-cloud-gateway
  cloud:
    gateway:
      routes: #给List<RouteDefinition>注入数据
        - id: service-consumer  #应用的标识
          uri: http://localhost:8082  #由网关路由到微服务的地址,也就是真实地址
          predicates:  #匹配规则
            - Path=/service-consumer/**  #可以写多个path
          filters:
            - AddResponseHeader=X-Response-Red, Blue #在响应中添加该响应头

eureka:
  client:
    service-url:
      defaultZone: http://192.168.2.140:8000/eureka  #设置eureka(注册中心集群)
  • 测试RewritePath
spring:
  application:
    name: spring-cloud-gateway
  cloud:
    gateway:
      routes: #给List<RouteDefinition>注入数据
        - id: service-consumer  #应用的标识
          uri: http://localhost:8082  #由网关路由到微服务的地址,也就是真实地址
          predicates:  #匹配规则
            - Path=/service-consumer/**  #可以写多个path
          filters:
            - AddResponseHeader=X-Response-Red, Blue #在响应中添加该响应头
            - RewritePath=/service-consumer/(?<subPath>/?.*), /consumer/$\{subPath}

eureka:
  client:
    service-url:
      defaultZone: http://192.168.2.140:8000/eureka  #设置eureka(注册中心集群)

RewritePath=/service-consumer/(?/?.*), /consumer/${subPath}:当访问
http://localhost:8080/service-consumer/product/110
,该路径首先匹配 Path,然后将原来的service-consumer/product/110替换为 /consumer/product/110 ,拼接为:http://localhost:8080/consumer/product/110

自定义局部过滤器

修改application.yml:

spring:
  application:
    name: spring-cloud-gateway
  cloud:
    gateway:
      routes: #给List<RouteDefinition>注入数据
        - id: service-consumer  #应用的标识
          uri: http://localhost:8082  #由网关路由到微服务的地址,也就是真实地址
          predicates:  #匹配规则
            - Path=/service-consumer/**  #可以写多个path
          filters:
            - AddResponseHeader=X-Response-Red, Blue #在响应中添加该响应头
            - RewritePath=/service-consumer/(?<subPath>/?.*), /consumer/$\{subPath}
            - MyParam=gender,male
             
eureka:
  client:
    service-url:
      defaultZone: http://192.168.2.140:8000/eureka  #设置eureka(注册中心集群)
  • 新建一个MyParamGatewayFilterFactory
/**
* 如果请求中含有gender=male会将参数值替换为female
* 例如:
* GET http://localhost:10000/feign/product/200?gender=female
* 将来controller中接收到的参数是gender=male而不是female
*/
@Component
public class MyParamGatewayFilterFactory extends
AbstractNameValueGatewayFilterFactory {
	
	@Override
	public GatewayFilter apply(NameValueConfig config) {
		return new GatewayFilter() {
			@Override
			public Mono<Void> filter(ServerWebExchange exchange,GatewayFilterChain chain) {
	ServerHttpRequest request = exchange.getRequest();
	MultiValueMap<String, String> queryParams =exchange.getRequest().getQueryParams();
	if (queryParams.containsKey("gender") &&
"female".equals(queryParams.getFirst("gender"))) {
/*如果请求参数中包含gender,并且参数值为female*/
MultiValueMap<String, String> newParamsMap = new
LinkedMultiValueMap<>(queryParams);
newParamsMap.set(config.getName(),config.getValue());
/*使用修改后的查询参数,重新构建一个URI*/
URI uri =
UriComponentsBuilder.fromUri(request.getURI()).replaceQueryParams(newParamsMap).
build().toUri();
/*使用新的URI构建一个新的请求对象*/
ServerHttpRequest updatedRequest =
request.mutate().uri(uri).build();
/*放行的时候将这个请求对象传递给Controller处理器*/
return
chain.filter(exchange.mutate().request(updatedRequest).build());
} else {
return chain.filter(exchange);
}
}
@Override
public String toString() {
return filterToStringCreator(MyParamGatewayFilterFactory.this)
.append(config.getName(), config.getValue()).toString();
				}
		};
		}
	}
}
  • 修改hystrix-feign-consumer(消费者)的controller
@RestController
@RequestMapping("/consumer/product")
public class HystrixFeignController {

@Autowired
ProductFeignService productFeignService;

@GetMapping("/{pid}")
public Result findByPid(@PathVariable Integer pid,String gender) {
System.out.println(gender);
return productFeignService.findByPid(pid);
}

@GetMapping("/list")
public Result findAll(HttpServletRequest request) {
System.out.println(request.getHeader("X-Request-blue"));
return productFeignService.findAll();
  }
}
  • 启动测试
GET http://localhost:10000/feign/product/200?gender=female

###
  • 自定义全局过滤器
    实现类似所有微服务都需要认证的功能,我们在访问微服务前可以判断是否携带token(一般用户登录后会生成一个token)
    如果有正常访问服务,否则提示没有权限
  • 新建TokenGlobalFilter
@Component
public class TokenGlobalFilter implements GlobalFilter, Ordered {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain
chain) {
String token =
exchange.getRequest().getQueryParams().getFirst("token");
if (StringUtils.isBlank(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();//完成请求
}
return chain.filter(exchange);
}
	@Override
	public int getOrder() {
	return 0;//返回值越小执行的优先级越高,权重
	}
}
  • 测试:
### 访问网关
GET http://localhost:10000/feign/product/200?gender=female&token=123123
###

Spring Cloud Stream

概述

在实际开发过程中,服务与服务之间通信经常会使用到消息中间件,而以往使用了哪个中间件比如RabbitMQ,那么该中间件和系统的耦合性就会非常高,如果我们要替换为Kafka那么变动会比较大,使用Spring Cloud Stream来整合我们的消息中间件,可以降低系统和中间件的耦合性。
在这里插入图片描述

用来操作消息中间件的

  • 15
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值