SpringCloud

SpringCloud

文章推荐:Eureka:Spring Cloud服务注册与发现组件(非常详细) (biancheng.net)

概述

Spring Cloud 是一个服务治理平台,是若干个框架的集合,提供了全套的分布式系统解决方案。包含了:服务注册与发现、配置中心、服务网关、智能路由、负载均衡、断路器、监控跟踪、分布式消息队列等等。

Spring Cloud 通过 Spring Boot 风格的封装,屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、容易部署的分布式系统开发工具包。开发者可以快速的启动服务或构建应用、同时能够快速和云平台资源进行对接。微服务是可以独立部署、水平扩展、独立访问(或者有独立的数据库)的服务单元,Spring Cloud 就是这些微服务的大管家,采用了微服务这种架构之后,项目的数量会非常多,Spring Cloud 做为大管家需要管理好这些微服务,自然需要很多小弟来帮忙。

SpringCloud常用组件表

服务的注册和发现(eureka,nacos,consul)

服务的负载均衡(ribbon,dubbo)

服务的相互调用(openFeign,dubbo)

服务的容错(hystrix,sentinel)

服务网关(gateway,zuul)

服务配置的统一管理(config-server,nacos,apollo)

服务消息总线(bus)

服务安全组件(security,Oauth2.0)

服务监控(admin)(jvm)

链路追综(sleuth+zipkin)

SpringCloud Alibaba与SpringCloud Netflix对照

注册和发现中心

Eureka快速入门

什么是CAP原则?

Eureka和zookeeper的区别

CAP原则是指一个分布式系统中,一致性,可用性,分区容错性

一致性:多个节点的数据保持一致。 (consistent)

可用性:当一个节点发生异常不可用之后,其他的节点任然可以提供服务。 available

分区容错性:由于每个节点存在的机房或者是分区不一样,存在数据传输时间的消耗,所以每个节点上的数据可能会短暂的不一致。 partition

CAP原则指的是,这三个要素最多只能同时存在实现两个,不可能三者兼顾。但是每一个分布式系统中都会存在P原则,所以通常只会出现CP和AP组合。

Zookeeper:CP 注重的数据的一致性,当节点发生异常不可用时,可能会造成几分钟无法访问

Eureka:AP 注重的时可可用,但是可能用户访问的数据存在一定的差异。

Eureka快速入门

搭建一个组测中心

创建Eureka-server

导入相关的依赖,注意SpringCloud有相应对应的版本。不要随意的搭配boot和Cloud的版本

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.12.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

书写application.yml配置文件

server:
  port: 8761 #默认端口

spring:
  application:
    name: eureka-server  #之前我们很少的指定过模块的名称,这里通常指定。

在启动类上开启@EnableEurekaServer //开启Euraka注册中心的功能

访问localhost:8761 表示Eureka-server注册成功

创建Eureka客户端

导入依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.12.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
    <java.version>17</java.version>
    <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

改写配置文件:

server:
  port: 8081

spring:
  application:
    name: eureka-client-a
#客户端需要将自己的信息注册(告诉)服务器(server)
#发送到哪?
eureka:
  client:
    service-url:  #指定注册地址
      defaultZone: http://localhost:8761/eureka

启动类上开启服务@EnableEurekaClient,

如果我们想将同一台实列注册多次,只需要修改配置文件中端口就行了。

Eureka配置文件介绍

了解配置文件,我们需要知道Eureka-server需要做一些什么?

作为注册中心(房东)

  1. 需要有一个服务列表(容器)保存注册的应用的信息。
  2. 需要知道自己的下面注册的应用是否还在线(心跳机制)
  3. 应用A和应用B需要联系,不能总是的通过Eureka-serve实现联系,需要在应用的本地保存一份注册列表。这时需要考虑是否可以容忍脏读。
  4. 如果某段时间内有大量的应用没有联系自己,这时Eureka-server会认为自己出现了问题。体现Ap原则

Eureka-server配置文件实列

# eureka-server的配置文件主要分为三类: server类  client类 instance类
eureka:
  server:
    eviction-interval-timer-in-ms: 10000 #服务间隔多少时间之后进行一个删除操作,当一个应用在该时间之内没有与服务器进行联系的话,服务器会认为应用已经下线
    renewal-percent-threshold: 0.85 #续约百分比,当超过规定范围的应用没有联系服务器时,服务器会认为自己出现了问题。
  instance:
    hostname: localhost
    instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port} #实列id
    prefer-ip-address: true #以ip形式显示具体的服务信息
    lease-renewal-interval-in-seconds: 5 #指示 eureka 客户端需要多久(以秒为单位)向 eureka 服务器发送检测信号,这个时间应该小于eviction-interval-timer-in-ms

Eureka-client配置文件

#客户端需要将自己的信息注册(告诉)服务器(server)
#发送到哪?
eureka:
  client:
    service-url:  #指定注册地址
      defaultZone: http://localhost:8761/eureka
    register-with-eureka: true #是否注册到注册中心上去
    fetch-registry: true #应用是否拉取服务列表到本地
    registry-fetch-interval-seconds: 10 #间隔多少时间拉取列表到本地
  instance:
    lease-renewal-interval-in-seconds: 5
    hostname: localhost
    prefer-ip-address: true
    instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}

分布式系统一致性算法

在我们分布式系统中,存在多个系统之间实现数据的集群,采用CP一致性算法保证每个节点数据的一致性的问题。比如Eureka、Zookeeper、Nacos实现集群都必须保证每个节点数据同步性的问题。

Zookeeper基于ZAP协议实现保证每个节点数据同步的问题,中心化思想集群模式。分为领导和跟随者角色。(主从模式)

Eureka基于AP模式实现注册中心,去中心化的思想、每个节点都是对等的,采用你中有我,我中有你的形式实现注册中心。

常见分布式一致性算法:

  1. ZAP协议(底层就是基于Paxos实现),核心底层基于2PC两阶段提交协议实现。
  2. Nacos中集群保证一致性算法采ratf协议模式,采用心跳机制实现选举的。Raft (thesecretlivesofdata.com)
  3. Eureka没有分布式数据一致性的机制 节点都是相同的

Eureka运行的理解

Eureka服务注册、下线、续约、剥离都是注册列表的CRUD

Eureka注册

Eureka客户端的register源码解释:

boolean register() throws Throwable {
    logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
    EurekaHttpResponse<Void> httpResponse;
    try {
        //注册的主要实现调用,instanceInfo读取配置文件配置项(ip,port,hostname)
        httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
    } catch (Exception e) {
        logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
        throw e;
    }
    if (logger.isInfoEnabled()) {
        logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
    }
    return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}

eurekaTransport.registrationClient.register(instanceInfo);调用register解释

@Override
public EurekaHttpResponse<Void> register(InstanceInfo info) {
    String urlPath = "apps/" + info.getAppName();
    ClientResponse response = null;
    try {
        Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
        addExtraHeaders(resourceBuilder);
        //发送post请求给urlPath地址,restFul风格post表示增加
        response = resourceBuilder
            .header("Accept-Encoding", "gzip")
            .type(MediaType.APPLICATION_JSON_TYPE)
            .accept(MediaType.APPLICATION_JSON)
            .post(ClientResponse.class, info);
        return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
    } finally {
        if (logger.isDebugEnabled()) {
            logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
                         response == null ? "N/A" : response.getStatus());
        }
        if (response != null) {
            response.close();
        }
    }
}

客服端发送post请求之后,那么服务端是如何处理保存Client信息的

注册表结构的第一个Key是应用名称(全大写) spring.application.name

Value中的key是应用的实例id Eureka.instance.instance-id

Value中的value是具体的服务节点信息

private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
    = new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
    read.lock();
    try {
        Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
        REGISTER.increment(isReplication);
        if (gMap == null) {
            final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
            gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
            if (gMap == null) {
                gMap = gNewMap;
            }
        }
        Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
        ……………………………………………………………………………………………………………………………………………………………………
    } finally {
        read.unlock();
    }
}
Eureka续约
@Override
public EurekaHttpResponse<InstanceInfo> sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus) {
    String urlPath = "apps/" + appName + '/' + id;
    ClientResponse response = null;
    try {
        WebResource webResource = jerseyClient.resource(serviceUrl)
            .path(urlPath)
            .queryParam("status", info.getStatus().toString())
            .queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString());
        if (overriddenStatus != null) {
            webResource = webResource.queryParam("overriddenstatus", overriddenStatus.name());
        }
        Builder requestBuilder = webResource.getRequestBuilder();
        addExtraHeaders(requestBuilder);
        //发送put请求给服务器端
        response = requestBuilder.put(ClientResponse.class);
        EurekaHttpResponseBuilder<InstanceInfo> eurekaResponseBuilder = anEurekaHttpResponse(response.getStatus(), InstanceInfo.class).headers(headersOf(response));
        if (response.hasEntity() &&
            !HTML.equals(response.getType().getSubtype())) { //don't try and deserialize random html errors from the server
            eurekaResponseBuilder.entity(response.getEntity(InstanceInfo.class));
        }
        return eurekaResponseBuilder.build();
    } finally {
        if (logger.isDebugEnabled()) {
            logger.debug("Jersey HTTP PUT {}/{}; statusCode={}", serviceUrl, urlPath, response == null ? "N/A" : response.getStatus());
        }
        if (response != null) {
            response.close();
        }
    }
}

Eureka的剥除也是一样发送相应的delete请求给服务器端。

服务发现

什么是服务发现?
通过服务名称获取到服务具体实例的过程。

@GetMapping("/test")
public String doDiscovery(String serverName){
    //通过服务名称获取相应的服务实例
    //注意返回值是一个list集合类型,因为一个实例名称可以开启多个实列,实现一个集群的实现
    List<ServiceInstance> instances = discoveryClient.getInstances(serverName);
    instances.forEach(System.out::println);
    ServiceInstance serviceInstance = instances.get(0);
    String host = serviceInstance.getHost();
    int port = serviceInstance.getPort();
    String url="http://"+host+port;
    //使用restTemplate发送Http请求给url就可以了
    return serviceInstance.toString();
}

返回结果:从中我们可以通过服务名称获取到服务的端口和hostName,通过字符串的拼接就可以实现发送Http请求给相应的地址,只需知道相应API的功能就可以了。

[EurekaDiscoveryClient.EurekaServiceInstance@3aa089fe instance = InstanceInfo [instanceId = localhost:eureka-client-b:8082, appName = EUREKA-CLIENT-B, hostName = 192.168.117.1, status = UP, ipAddr = 192.168.117.1, port = 8082, securePort = 443, dataCenterInfo = com.netflix.appinfo.MyDataCenterInfo@276b4a2]

RestTemplate

RestTemplateApi介绍

void RestTemplateApi(){
    String url="";
    String data="";
    //Entity  将会返回消息的完整消息(包括状态码,相应的结果)
    //Object  只会返回相应的结果
    restTemplate.getForEntity(url,String.class);
    restTemplate.getForObject(url,String.class);
    restTemplate.postForEntity(url,data,String.class);
    restTemplate.postForObject(url,data,String.class);
    restTemplate.put(url,String.class);
    restTemplate.delete(url);
}

ribbon

负载均衡

在任何一个系统中,负载均衡都是一个十分重要且不得不去实施的内容,它是系统处理高并发、缓解网络压力和服务端扩容的重要手段之一。

负载均衡(Load Balance) ,简单点说就是将用户的请求平摊分配到多个服务器上运行,以达到扩展服务器带宽、增强数据处理能力、增加吞吐量、提高网络的可用性和灵活性的目的。

常见的负载均衡算法:

  1. 轮询分发
  2. 固定ip
  3. 随机分发
  4. 权重分发
  5. Hash

常见的负载均衡方式有两种:

  • 服务端负载均衡
  • 客户端负载均衡

服务端负载均衡

服务端负载均衡是最常见的负载均衡方式,其工作原理如下图。

客户端负载均衡

相较于服务端负载均衡,客户端服务在均衡则是一个比较小众的概念。

客户端负载均衡的工作原理如下图。

客户端负载均衡是将负载均衡逻辑以代码的形式封装到客户端上,即负载均衡器位于客户端。客户端通过服务注册中心(例如 Eureka Server)获取到一份服务端提供的可用服务清单。有了服务清单后,负载均衡器会在客户端发送请求前通过负载均衡算法选择一个服务端实例再进行访问,以达到负载均衡的目的;

客户端负载均衡具有以下特点:

  • 负载均衡器位于客户端,不需要单独搭建一个负载均衡服务器。
  • 负载均衡是在客户端发送请求前进行的,因此客户端清楚地知道是哪个服务端提供的服务。
  • 客户端都维护了一份可用服务清单,而这份清单都是从服务注册中心获取的。

Ribbon入门

<!--Spring Cloud Ribbon 依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

使用Ribbon+RestTemplate

@Bean //将 RestTemplate 注入到容器中
@LoadBalanced //在客户端使用 RestTemplate 请求服务端时,开启负载均衡(Ribbon)
public RestTemplate restTemplate() {
    return new RestTemplate();
}

使用LoadBalancerClient

1.注入LoadBalancerClient

@Autowired
private LoadBalancerClient loadBalancerClient;

2.调用loadBalancerClient或者加了 @LoadBalanced注解的RestTemplate

@GetMapping("/ribbon")
public URI ribbon(){
    // 通过地址自动负载均衡
    ServiceInstance choose = loadBalancerClient.choose("provider");
    return choose.getUri();
}

在consumer发送发送请求给provider时只需要将原来的URL中的IP使用应用名称来代替。

Ribbon为我们所做的事情

  • 将我们的请求拦截。获取当中的服务名称。
  • 通过服务名称结合Eureka的服务发现,获取到服务列表
  • 通过服务列表使用负载均衡算法,获取相应的ip和port
  • 重构URL
  • 发送Http请求

Ribbon 实现负载均衡

Ribbon 是一个客户端的负载均衡器,它可以与 Eureka 配合使用轻松地实现客户端的负载均衡。Ribbon 会先从 Eureka Server(服务注册中心)去获取服务端列表,然后通过负载均衡策略将请求分摊给多个服务端,从而达到负载均衡的目的。

Spring Cloud Ribbon 提供了一个 IRule 接口,该接口主要用来定义负载均衡策略,它有 7 个默认实现类,每一个实现类都是一种负载均衡策略。

序号 实现类 负载均衡策略
1 RoundRobinRule 按照线性轮询策略,即按照一定的顺序依次选取服务实例
2 RandomRule 随机选取一个服务实例
3 RetryRule 按照 RoundRobinRule(轮询)的策略来获取服务,如果获取的服务实例为 null 或已经失效,则在指定的时间之内不断地进行重试(重试时获取服务的策略还是 RoundRobinRule 中定义的策略),如果超过指定时间依然没获取到服务实例则返回 null 。
4 WeightedResponseTimeRule WeightedResponseTimeRule 是 RoundRobinRule 的一个子类,它对 RoundRobinRule 的功能进行了扩展。 根据平均响应时间,来计算所有服务实例的权重,响应时间越短的服务实例权重越高,被选中的概率越大。刚启动时,如果统计信息不足,则使用线性轮询策略,等信息足够时,再切换到 WeightedResponseTimeRule。
5 BestAvailableRule 继承自 ClientConfigEnabledRoundRobinRule。先过滤点故障或失效的服务实例,然后再选择并发量最小的服务实例。
6 AvailabilityFilteringRule 先过滤掉故障或失效的服务实例,然后再选择并发量较小的服务实例。
7 ZoneAvoidanceRule 默认的负载均衡策略,综合判断服务所在区域(zone)的性能和服务(server)的可用性,来选择服务实例。在没有区域的环境下,该策略与轮询(RandomRule)策略类似。

探究一些ZoneAvoidanceRule,通过调用父类PredicateBasedRule的Choose函数,服务数量进行一个取模运算。

为了保持线程安全,Ribbon使用CAS,和原子类保证了线程的安全。

private int incrementAndGetModulo(int modulo) {
    for (;;) {
        int current = nextIndex.get();
        int next = (current + 1) % modulo;
        if (nextIndex.compareAndSet(current, next) && current < modulo)
            return current;
    }
}

更改负载均衡算法

指定不同的服务使用不同的负载均衡算法
#访问不同的服务可以使用不同的算法规则
provider:  #先写服务提供者的应用名称
  rabbion:
     NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule 
更改全局的负载均衡算法
@Bean
public IRule myRule(){
    return new RandomRule();
}

由于IRule是一个接口,所以我可以通过实现这个接口,自定义自己的负载均衡算法。

OpenFeign:Spring Cloud声明式服务调用组件

OpenFeign是一个Web声明式的Http客户端调用工具,提供接口和注解形式调用

OpenFeign是一个声明式RESTful网络请求客户端。OpenFeign会根据带有注解的函数信息构建出网络请求的模板,在发送网络请求之前,OpenFeign会将函数的参数值设置到这些请求模板中。虽然OpenFeign只能支持基于文本的网络请求,但是它可以极大简化网络请求的实现,方便编程人员快速构建自己的网络请求应用

OpenFeign 常用注解

使用 OpenFegin 进行远程服务调用时,常用注解如下表。

注解 说明
@FeignClient 该注解用于通知 OpenFeign 组件对 @RequestMapping 注解下的接口进行解析,并通过动态代理的方式产生实现类,实现负载均衡和服务调用。
@EnableFeignClients 该注解用于开启 OpenFeign 功能,当 Spring Cloud 应用启动时,OpenFeign 会扫描标有 @FeignClient 注解的接口,生成代理并注册到 Spring 容器中。
@RequestMapping Spring MVC 注解,在 Spring MVC 中使用该注解映射请求,通过它来指定控制器(Controller)可以处理哪些 URL 请求,相当于 Servlet 中 web.xml 的配置。
@GetMapping Spring MVC 注解,用来映射 GET 请求,它是一个组合注解,相当于 @RequestMapping(method = RequestMethod.GET) 。
@PostMapping Spring MVC 注解,用来映射 POST 请求,它是一个组合注解,相当于 @RequestMapping(method = RequestMethod.POST) 。

OpenFeign的入门

<!--添加 OpenFeign 依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

开启OpenFeign服务@EnableFeignClients

简单应用

使用OpenFeign,UserServer远程调用OrderServer中DoOrder接口

OrderServer

@RestController
public class OrderController {
    @GetMapping("/DoOrder")
    public String DoOrder(){
        return  "用户下了订单";
    }
}

在UserServer中需要定义一个接口

@FeignClient(value = "eureka-client-a") //服务名称
@Component
public interface UserOrderFeign {
    //需要远程调用服务方法的方法签名
    @GetMapping("/DoOrder")
    public String DoOrder();
}
@RestController
public class UserDoOrder {
    @Autowired
    private UserOrderFeign userOrderFeign;
    @GetMapping("/UserDoOrder")
    public String UserDoOrder(){
        return userOrderFeign.DoOrder();
    }
}

注意点:如果在我们请求订单模块时,订单模块需要对数据库进行操作,可能会比较的消耗时间。那么我们的UserOrder会不会出现超时异常,通过实验,结果是UserOrder会抛出一个超时异常。

修改OpenFeign远程调用的超时时间

由于OpenFeign的原理是将Ribbon进行一个封装,所以如果我们希望修改OpenFeign的超时时间的话,其实质是修改Ribbon的配置。默认超时时间默认是1S

ribbon:
  ReadTimeout: 3000 #访问超时时间
  ConnectTimeout: 3000 #连接超时时间

OpenFeign核心探索

OPenFeign使用一个注解就可以实现远程调用是如何做到

可以猜测这个接口一个会议代理对象,我们知道只有两种代理的方式(JDK动态代理,cglib动态代理)

JDK动态代理为接口创建代理实例

CGLIB通过继承方式实现代理

所以OPenFeign一定是采用的是JDK动态代理生成代理对象的

   @Test
void contextLoads() {
    UserOrderFeign o = (UserOrderFeign)Proxy.newProxyInstance(EurekaClientBApplication.class.getClassLoader(), new Class[]{UserOrderFeign.class}, new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            /*
                * 通过代理获取到方法注解上的API名称
                * 通过获取接口上方的FeignClient()中的Value,应用的名称,结合上Eureka的方法发现
                * 通过拼接URL,使用Ribbon向URL接口发送请求就可以实现远程调用
                * */
            //获取API
            GetMapping annotation = method.getAnnotation(GetMapping.class);
            String[] Api = annotation.value();
            String API = Api[0];
            //获取应用名称
            Class<?> aClass = method.getDeclaringClass();
            FeignClient feignClient = aClass.getAnnotation(FeignClient.class);
            String appName = feignClient.value();
            //拼接URL
            String url="http://"+appName+"/"+API;
            //使用Ribbon发送请求实现负载均衡
            String forObject = restTemplate.getForObject(url, String.class);
            return forObject;
        }
    });
    //使用JDK动态代理所以一定会调用invoke方法,类似与AOP机制
    String s = o.DoOrder();
    System.out.println(s);
}

OpenFeign 日志增强

Logger.Level 的具体级别如下:

  • NONE:不记录任何信息。
  • BASIC:仅记录请求方法、URL 以及响应状态码和执行时间。
  • HEADERS:除了记录 BASIC 级别的信息外,还会记录请求和响应的头信息。
  • FULL:记录所有请求与响应的明细,包括头信息、请求体、元数据等等。
  /**
   * Controls the level of logging.
   */
  public enum Level {
    /**
     * No logging.
     */
    NONE,
    /**
     * Log only the request method and URL and the response status code and execution time.
     */
    BASIC,
    /**
     * Log the basic information along with request and response headers.
     */
    HEADERS,
    /**
     * Log the headers, body, and metadata for both requests and responses.
     */
    FULL
  }

在配置文件中开启接口的日志级别

logging:
  level:
    #feign 日志以什么样的级别监控该接口
    net.biancheng.c.service.DeptFeignService: debug
@Bean
public Logger.Level level(){
    return Logger.Level.FULL;
}

Hystrix:Spring Cloud服务熔断与降级组件

服务雪崩

在一个微服务系统中,我们的一个服务可能是一个链式调用的,A->B->C如果C发生了宕机的话,那么A,B中的线程只会在等待超时之后才会将线程回收,在并发量很大的时候,就会出现服务线程无法及时的回收,导致整个服务器出现崩溃。

解决方案:

  • 调整我们的超时时间,将超时时间缩短就可以做到及时的回收我们的线程。缺点如果服务器中本身就存在服务耗时比较长,这种方式就会影响到我们正常的服务了。
  • 如果我们上层的服务知道下层的服务已经发生了宕机的话,那么我之后的请求就可以直接的返回,不需要等待超时时间。及时回收线程。

这种方式并不是保证服务的正常服务的,知道保证当存在一个服务宕机时,缓解服务器压力。

熔断器

熔断器(Circuit Breaker)一词来源物理学中的电路知识,它的作用是当线路出现故障时,迅速切断电源以保护电路的安全。

与物理学中的熔断器作用相似,微服务架构中的熔断器能够在某个服务发生故障后,向服务调用方返回一个符合预期的、可处理的降级响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常。这样就保证了服务调用方的线程不会被长时间、不必要地占用,避免故障在微服务系统中的蔓延,防止系统雪崩效应的发生。

Hystrix的基本使用

导入依赖

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

书写一个Hystrix实现类作为超时失败后的备选方案

@Component
public class UserOrderHystrix implements UserOrderFeign {
    @Override
    public String DoOrder() {
        return "我是服务超时时的备选方案";
    }
}

当然只有经过了远程调用的方法才会需要一个熔断所以需要在Feign接口上面指定失败的回调类是什么

@FeignClient(value = "eureka-client-a",fallback = UserOrderHystrix.class) //服务名称
public interface UserOrderFeign {
    @GetMapping("/DoOrder")
    public String DoOrder();
}

还需要开启Hystrix服务

@EnableHystrix

feign:
  hystrix:
    enabled: true #在SpringCloud F版本之前是默认开启的。

hystrix的配置文件

隔离级别默认是使用thread

thread消费者会为每个提供者分配好线程(默认是10个),

优点: 每个线程都有自己的线程组,高度隔离,互不影响

缺点:存在线程的切换,效率比较低

场景:并发量比较大的场景

Semaphore:

优点:不会存在线程的切换,效率比较高。

缺点:但是提供者之间存在影响。

并发量比较小,内部调用。

hystrix: #hystrix的全局控制
  command:
    default: #default是全局控制,也可以换成的单个方法控制,把default换成方法名
      circuitBreaker:
        enabled: true #开启短路器
        requestVolumeThreshold: 3 #失败次数(阈值) 10次
        sleepWindowInMilliseconds: 20000 #窗口时间
        errorThresholdPercentage: 60 #失败率
      execution:
        isolation:
          Strategy: thread #隔离方式thread线程隔离集合和semaphore信号量隔离
          thread:
            timeoutInMilliseconds: 3000 #调用超时时长
      fallback:
        isolation:
          semaphore:
            maxConcurrentRequests: 1000 #信号量隔离级别最大并发数

上面是全部配置。

某一个方法配置,在方法使用

@HystrixCommand(fallbackMethod = "deptCircuitBreaker_fallback", commandProperties = {
        //以下参数在 HystrixCommandProperties 类中有默认配置
        @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), //是否开启熔断器
    @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds",value = "1000"), //统计时间窗
        @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), //统计时间窗内请求次数
        @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), //休眠时间窗口期
        @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"), //在统计时间窗口期以内,请求失败率达到 60% 时进入熔断状态
})

Hystrix 服务熔断

熔断机制是为了应对雪崩效应而出现的一种微服务链路保护机制。

当微服务系统中的某个微服务不可用或响应时间太长时,为了保护系统的整体可用性,熔断器会暂时切断请求对该服务的调用,并快速返回一个友好的错误响应。这种熔断状态不是永久的,在经历了一定的时间后,熔断器会再次检测该微服务是否恢复正常,若服务恢复正常则恢复其调用链路。

熔断状态

在熔断机制中涉及了三种熔断状态:

  • 熔断关闭状态(Closed):当服务访问正常时,熔断器处于关闭状态,服务调用方可以正常地对服务进行调用。
  • 熔断开启状态(Open):默认情况下,在固定时间内接口调用出错比率达到一个阈值(例如 50%),熔断器会进入熔断开启状态。进入熔断状态后,后续对该服务的调用都会被切断,熔断器会执行本地的降级(FallBack)方法。
  • 半熔断状态(Half-Open): 在熔断开启一段时间之后,熔断器会进入半熔断状态。在半熔断状态下,熔断器会尝试恢复服务调用方对服务的调用,允许部分请求调用该服务,并监控其调用成功率。如果成功率达到预期,则说明服务已恢复正常,熔断器进入关闭状态;如果成功率仍旧很低,则重新进入熔断开启状态。

三种熔断状态之间的转化关系如下图:

Hystrix 实现熔断机制

在 Spring Cloud 中,熔断机制是通过 Hystrix 实现的。Hystrix 会监控微服务间调用的状况,当失败调用到一定比例时(例如 5 秒内失败 20 次),就会启动熔断机制。

Hystrix 实现服务熔断的步骤如下:

  1. 当服务的调用出错率达到或超过 Hystix 规定的比率(默认为 50%)后,熔断器进入熔断开启状态。
  2. 熔断器进入熔断开启状态后,Hystrix 会启动一个休眠时间窗,在这个时间窗内,该服务的降级逻辑会临时充当业务主逻辑,而原来的业务主逻辑不可用。
  3. 当有请求再次调用该服务时,会直接调用降级逻辑快速地返回失败响应,以避免系统雪崩。
  4. 当休眠时间窗到期后,Hystrix 会进入半熔断转态,允许部分请求对服务原来的主业务逻辑进行调用,并监控其调用成功率。
  5. 如果调用成功率达到预期,则说明服务已恢复正常,Hystrix 进入熔断关闭状态,服务原来的主业务逻辑恢复;否则 Hystrix 重新进入熔断开启状态,休眠时间窗口重新计时,继续重复第 2 到第 5 步。

第一个微服务架构

sleuth:链路追踪

什么是链路追踪

单纯的理解链路追踪,就是指一次任务的开始到结束,期间调用的所有系统及耗时(时间跨度)都可以完整记录下来。

zipkin

Zipkin是Twitter开源的调用链分析工具,目前基于springcloud sleuth得到了广泛的使用,特点是轻量,使用部署简单。用于展示链路情况。

基本使用

导入sleuth依赖:因为sleth需要记录没有一次的调用情况,所以所有的consumer-server和provider-server基本需要导入依赖。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

zipkin下载

Spring Cloud Edgware 版本之后,改为强制采用官方提供的 Jar 包的形式启动。
下载地址:https://repo1.maven.org/maven2/io/zipkin/zipkin-server/

zipkin启动

添加配置

spring:
  application:
    name: user-service
  zipkin:
    base-url: http://localhost:9411
  sleuth:
    sampler:
      probability: 1 #配置采样率 默认的采样比例为:0.1,即10%,所设置的值介于0 到 1,1表示会全部采集
      rate: 10 #为了使用速率限制采样器,选择每秒间隔接受trace量,最小数字为0

将所有的项目启动之后访问http://127.0.0.1:9411/

Admin监控

功能

Spring Boot Admin提供了很多服务治理方面的功能,利用它能节省我们很多在治理服务方面的时间和精力Spring Boot Admin提供了如下功能(包括但不限于):

  • 显示健康状态及详细信息,如JVM和内存指标、数据源指标、缓存指标
  • 跟踪并下载日志文件
  • 查看jvm系统-和环境属性
  • 查看Spring启动配置属性方便loglevel管理
  • 查看线程转储视图http-traces
  • 查看http端点查看计划任务
  • 查看和删除活动会话(使用spring-session)
  • 状态更改通知(通过电子邮件、Slack、Hipchat…)
  • 状态变化的事件日志(非持久性)
  • 下载 heapdump
  • 查看 Spring Boot 配置属性
  • 支持 Spring Cloud 的环境端点和刷新端点
  • 支持 K8s
  • 易用的日志级别管理
  • 与JMX-beans交互
  • 查看线程转储
  • 查看http跟踪
  • 查看auditevents
  • 查看http-endpoints
  • 查看计划任务
  • 查看和删除活动会话(使用 Spring Session )
  • 查看Flyway/Liquibase数据库迁移
  • 状态变更通知(通过电子邮件,Slack,Hipchat等,支持钉钉)
  • 状态更改的事件日志(非持久化)

用于管理和监视您的Spring Boot®应用程序。这些应用程序在我们的Spring Boot Admin Client中注册(通过HTTP),或者是通过Spring Cloud®(例如Eureka,Consul)发现的。 UI只是Spring Boot Actuator端点之上的Vue.js应用程序。

使用

admin存在连个端:一个service端一个是client端,在springBoot项目中使用 Spring Boot Admin,贼好使! – 掘金 (juejin.cn)

在SpringCloud项目中使用可以结合Eureka获取到所有服务的信息

配置开放所有监控项

# 开启监控所有项
management:
  endpoints:
         web:
           exposure:
                includem: "*"

注意这个配置并不是springBoot自带的,而是actuator依赖带的,我们需要在相应的server服务导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

最终效果

Gateway:Spring Cloud API网关组件

Gateway:Spring Cloud API网关组件(非常详细) (biancheng.net)

在微服务架构中,一个系统往往由多个微服务组成,而这些服务可能部署在不同机房、不同地区、不同域名下。这种情况下,客户端(例如浏览器、手机、软件工具等)想要直接请求这些服务,就需要知道它们具体的地址信息,例如 IP 地址、端口号等。

API 网关是一个搭建在客户端和微服务之间的服务,我们可以在 API 网关中处理一些非业务功能的逻辑,例如权限验证、监控、缓存、请求路由等。

API 网关就像整个微服务系统的门面一样,是系统对外的唯一入口。有了它,客户端会先将请求发送到 API 网关,然后由 API 网关根据请求的标识信息将请求转发到微服务实例。

所以可以猜测一下网关的功能应该需要有哪些

  1. 路由转发,所有的请求全部需要在网关这通过相应的方式找到具体的API,转发到具体的服务上。
  2. 安全方面,不需要直接将微服务里面的服务端口暴露出去。
  3. 负载均衡。一个服务如果有多台机器运行,那么我们需要负载均衡一下,缓解服务器压力。

作用:就是可以实现用户的验证登陆、解决跨域、日志拦截、权限控制、限流熔断、负载均衡、黑名单和白名单机制等。

Zuul与GateWay有那些区别

Zuul网关属于NetFix公司开源框架,属于第一代微服务网关

GateWay属于SpringCloud自己研发的网关框架,属于第二代微服务网关。相比来说GateWay比Zuul网关的性能要好很多。

Zuul 1.0网关底层基于Servlet实现,阻塞式(BIO)api,不支持长连接

Zuul2.0 NIO

SpringBoot-WebSpringCloudGateWay基于Spring5构建,能够实现响应式非阻塞式(NIO)api,支持长连接,能够更好的支持Spring体系产品,依赖SpringBoot-WebFux

springCloud没有集成和支持Zuul2.0

SpringCloudGateway是基于webFlux框架实现的,而webFlux框架底层则使用了高性能的Reactor模式通信框架的Netty

网关服务的端口号一般多少:80或者443

Spring Cloud Gateway 核心概念

Spring Cloud GateWay 最主要的功能就是路由转发,而在定义转发规则时主要涉及了以下三个核心概念,如下表。

核心概念 描述
Route(路由) 网关最基本的模块。它由一个 ID、一个目标 URI、一组断言(Predicate)和一组过滤器(Filter)组成。
Predicate(断言) 路由转发的判断条件,我们可以通过 Predicate 对 HTTP 请求进行匹配,例如请求方式、请求路径、请求头、参数等,如果请求与断言匹配成功,则将请求转发到相应的服务。
Filter(过滤器) 过滤器,我们可以使用它对请求进行拦截和修改,还可以使用它对上文的响应进行再处理。

Gateway 的工作流程

Spring Cloud Gateway 工作流程如下图。

Spring Cloud Gateway 工作流程说明如下:

  1. 客户端将请求发送到 Spring Cloud Gateway 上。
  2. Spring Cloud Gateway 通过 Gateway Handler Mapping 找到与请求相匹配的路由,将其发送给 Gateway Web Handler。
  3. Gateway Web Handler 通过指定的过滤器链(Filter Chain),将请求转发到实际的服务节点中,执行业务逻辑返回响应结果。
  4. 过滤器之间用虚线分开是因为过滤器可能会在转发请求之前(pre)或之后(post)执行业务逻辑。
  5. 过滤器(Filter)可以在请求被转发到服务端前,对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等。
  6. 过滤器可以在响应返回客户端之前,对响应进行拦截和再处理,例如修改响应内容或响应头、日志输出、流量监控等。
  7. 响应原路返回给客户端。

Nginx和Gateway区别

相同点:

都是可以实现api的拦截,负载均衡、反向代理、请求过滤,可以完全和网关实现一样的效果。

不同点:

Nginx性能好,并发量在30000到50000,使用C+lua编写。

GateWay,性能较差,并发量在10000,使用java编写

GateWay入门

导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

注意的当导入了GateWay的依赖之后,就不能导入spring-boot-starter-web依赖了,因为web默认的服务器是tomcat,而GataWay的服务器是Netty。

GateWay配置文件

server:
  port: 80
spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      enabled: true #只要添加了依赖默认开启
      routes:
        - id: user-server-route
          uri: http://localhost:88
          predicates:
            - Path=/UserDoOrder

通过代码的方式实现路由

package com.zl.config;

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GateWayConfig {
    /*
    * 代码实现和yml实现可以一起使用
    * */
    @Bean
    public RouteLocator routeLocator(RouteLocatorBuilder builder){
        return builder.routes()
                .route("dance-id",r->r.path("/v/dance").uri("https://www.bilibili.com"))
                .build();
    }
}

Spring Cloud Gateway 动态路由

默认情况下,Spring Cloud Gateway 会根据服务注册中心(例如 Eureka Server)中维护的服务列表,以服务名(spring.application.name)作为路径创建动态路由进行转发,从而实现动态路由功能。

我们可以在配置文件中,将 Route 的 uri 地址修改为以下形式。

lb://service-name

以上配置说明如下:

  • lb:uri 的协议,表示开启 Spring Cloud Gateway 的负载均衡功能。
  • service-name:服务名,Spring Cloud Gateway 会根据它获取到具体的微服务地址。
server:
  port: 81
spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      enabled: true #只要添加了依赖默认开启
      routes:
        - id: user-server-route
          uri: lb://user-service  #使用lb:将会实现一个负载均衡的效果。
          predicates:
            - Path=/UserDoOrder
      discovery:
        locator:
          enabled: true #开启动态路由,但是在访问API实现,需要在前面添加/应用名称/APi
          lower-case-service-id: true #开启服务名称小写
# 需要将GateWay服务注册到Eureka服务上面去,因为GateWay需要通过拉取服务列表,结合服务发现实现动态路由的效果。
eureka:
  client:
    service-url:  #指定注册地址
      defaultZone: http://localhost:8761/eureka
    register-with-eureka: true #是否注册到注册中心上去
    fetch-registry: true #应用是否拉取服务列表到本地
    registry-fetch-interval-seconds: 3 #间隔多少时间拉取列表到本地
  instance:
    lease-renewal-interval-in-seconds: 5
    hostname: localhost
    prefer-ip-address: true
    instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}

断言工厂predicate

Spring Cloud Gateway 中文文档 (springdoc.cn)

常见的 Predicate 断言如下表(假设转发的 URI 为 http://localhost:8001)。简单来说predicate就是给请求路由添加了一定限制条件。

断言 示例 说明
Path – Path=/dept/list/** 当请求路径与 /dept/list/** 匹配时,该请求才能被转发到 http://localhost:8001 上。
Before – Before=2021-10-20T11:47:34.255+08:00[Asia/Shanghai] 在 2021 年 10 月 20 日 11 时 47 分 34.255 秒之前的请求,才会被转发到 http://localhost:8001 上。
After – After=2021-10-20T11:47:34.255+08:00[Asia/Shanghai] 在 2021 年 10 月 20 日 11 时 47 分 34.255 秒之后的请求,才会被转发到 http://localhost:8001 上。
Between – Between=2021-10-20T15:18:33.226+08:00[Asia/Shanghai],2021-10-20T15:23:33.226+08:00[Asia/Shanghai] 在 2021 年 10 月 20 日 15 时 18 分 33.226 秒 到 2021 年 10 月 20 日 15 时 23 分 33.226 秒之间的请求,才会被转发到 http://localhost:8001 服务器上。
Cookie – Cookie=name,c.biancheng.net 携带 Cookie 且 Cookie 的内容为 name=c.biancheng.net 的请求,才会被转发到 http://localhost:8001 上。
Header – Header=X-Request-Id,\d+ 请求头上携带属性 X-Request-Id 且属性值为整数的请求,才会被转发到 http://localhost:8001 上。
Method – Method=GET 只有 GET 请求才会被转发到 http://localhost:8001 上。

Gateway过滤器

Filter 的分类

Spring Cloud Gateway 提供了以下两种类型的过滤器,可以对请求和响应进行精细化控制。

过滤器类型 说明
Pre 类型 这种过滤器在请求被转发到微服务之前可以对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等操作。
Post 类型 这种过滤器在微服务对请求做出响应后可以对响应进行拦截和再处理,例如修改响应内容或响应头、日志输出、流量监控等。

按照作用范围划分,Spring Cloud gateway 的 Filter 可以分为 2 类:

  • GatewayFilter:应用在单个路由或者一组路由上的过滤器。
  • GlobalFilter:应用在所有的路由上的过滤器。
自定义GlobalFilter过滤器
package com.zl.filter;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.RequestPath;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.HashMap;

@Component
public class myGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //可以通过exchange获取到相应的Request和Response
        //通过Request获取到请求的一下参数
        ServerHttpRequest request = exchange.getRequest();
        URI uri = request.getURI();
        RequestPath path = request.getPath();
        //当请求失败之后可以使用response发送相应的JSON数据给前端 forExample code:403 message: "你没有权限"
        ServerHttpResponse response = exchange.getResponse();
        response.getHeaders().set("content-type","application/json;charset=utf-8");
        HashMap<String, Object> map = new HashMap<>();
        map.put("code",200);
        map.put("message","你没有权限");
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            byte[] bytes = objectMapper.writeValueAsBytes(map);
            DataBuffer wrap = response.bufferFactory().wrap(bytes);
            return response.writeWith(Mono.just(wrap));
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
//        return chain.filter(exchange);
    }
   //配置当前过滤器的位置 数字越小越先执行
    @Override
    public int getOrder() {
        return 0;
    }
}
IP拦截
package com.zl.filter;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@Component
public class IpCheckFilter implements GlobalFilter, Ordered {
    public static final String blockList= "0:0:0:0:0:0:0:1";
    /*
    * 一般使用一个数据库来存储黑名单的IP,获取有的地方使用白名单。
    * 两种的区别
    * 黑名单表示在该名单中的IP是不可以进行访问的
    * 百名单表示只有这个名单中的IP是可以进行访问的。
    * 在网关中我不要做一下比较耗时的操作,比如查询数据库,因为网关并发量比较大。通常会将名单存储到redis中。
    * 如果是名单IP比较少时,直接将IP保存到内存中
    * */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        //获取到访问这个的IP 通常IPV6,
        String hostString = request.getRemoteAddress().getAddress().getHostName();
        System.out.println(hostString);
        //查询黑名单中是否存在该IP地址
        if(!blockList.equals(hostString)){
            return chain.filter(exchange);
        }
        ServerHttpResponse response = exchange.getResponse();
        HashMap<Object, Object> map = new HashMap<>();
        response.getHeaders().set("content-type","application/json;charset=utf-8");
        map.put("code",438);
        map.put("message","你是黑名单");
        ObjectMapper objectMapper = new ObjectMapper();
        byte[] bytes = new byte[0];
        try {
            bytes = objectMapper.writeValueAsBytes(map);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
        DataBuffer wrap = response.bufferFactory().wrap(bytes);
        return response.writeWith(Mono.just(wrap));
    }

    @Override
    public int getOrder() {
        return -5;
    }
}
自定义token拦截

@Component
/*
* 1、获取到url,判断该URL是否不需要验证token
* 2、获取请求头Authorization
* 3、判断是否为null
* 4、判断token是否存在与redis中
* 5、进行放行与拦截
* */
public class tokenFilter implements GlobalFilter, Ordered {
    private static List whiterList= Arrays.asList("/doLogin");
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        RequestPath url = exchange.getRequest().getPath();
        if(whiterList.contains(url)){
            return chain.filter(exchange);
        }
        HttpHeaders headers = exchange.getRequest().getHeaders();
        List<String> authorization = headers.get("Authorization");
        if(!CollectionUtils.isEmpty(authorization)){
            String token = authorization.get(0);
            if(StringUtils.hasText(token)){
                //判断token中是否存在token
                 if(redisTemplate.hasKey(token)){
                     return chain.filter(exchange);
                 }
            }
        }
        //对请求进行拦截
        ServerHttpResponse response = exchange.getResponse();
        Map<String,Object> map=new HashMap<>();
        map.put("code",401);
        map.put("msg","未授权");
        ObjectMapper objectMapper = new ObjectMapper();
        byte[] bytes = new byte[0];
        try {
            bytes = objectMapper.writeValueAsBytes(map);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
        DataBuffer wrap = response.bufferFactory().wrap(bytes);
        return response.writeWith(Mono.just(wrap));
    }

    @Override
    public int getOrder() {
        return 2;
    }
}
gateway集成redis做限流

spring cloud – Gateway整合Redis实现网关限流 – william_zhao – 博客园 (cnblogs.com)

什么是限流?

限流就是限制一段时间内,用户访问资源的次数,减轻服务器压力,主要分为两类:

1、IP限流(5s内同一个IP访问超过3次,则限制不让访问,过一段时间才可以继续访问)

2、请求量限流(只要一段时间内(窗口期),请求次数到达一个阀值,就直接拒绝后面来的访问)

gateway已经内置一个RequestRateLimiterGatewayFilterFactory,注意这是一个gatewayFilter过滤器所以只能针对的是某个API.需要添加依赖spring-boot-starter-data-redis-reactive

<!--RequestRateLimiter限流-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
令牌桶算法

令牌桶算法:随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100,则间隔是 10ms)往桶里加入 Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了。新请求来临时,会各自拿走一个 Token,如果没有 Token 可拿了就阻塞或者拒绝服务。

令牌桶的另外一个好处是可以方便的改变速度。一旦需要提高速率,则按需提高放入桶中的令牌的速率。一般会定时(比如 100 毫秒)往桶中增加一定数量的令牌,有些变种算法则实时的计算应该增加的令牌的数量。Guava 中的 RateLimiter 采用了令牌桶的算法,设计思路参见 How is the RateLimiter designed, and why?,详细的算法实现参见源码。

实现:

package com.zl.config;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import reactor.core.publisher.Mono;

@Configuration
public class RequestLimited {
    @Bean
    @Primary //作为主选方案
    public KeyResolver ipKeyResolver(){
        // 通过对IP进行限制
        return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostString());
    }
    @Bean
    public KeyResolver apiKeyResolver(){
        return exchange -> Mono.just(exchange.getRequest().getPath().toString());
    }
}

配置针对的API做限流

spring:
  application:
    name: gateway-server
  cloud:
    gateway:
      enabled: true #只要添加了依赖默认开启
      routes:
        - id: user-server-route
          uri: lb://user-service
          predicates:
            - Path=/UserDoOrder
          filters:
            - name: RequestRateLimiter
              args:
                # 用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
                key-resolver: "#{@ipKeyResolver}"
                # 令牌桶每秒填充平均速率,即行等价于允许用户每秒处理多少个请求平均数
                redis-rate-limiter.replenishRate: 1
                # 令牌桶的容量,允许在一秒钟内完成的最大请求数
                redis-rate-limiter.burstCapacity: 2
gateweay进行跨域
package com.zl.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;

@Configuration
public class CorsConfig {
    @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*");//允许所有请求头
        config.addAllowedOrigin("*");//允许所有请求方法,例如get,post等
        config.addAllowedHeader("*");//允许所有的请求来源
        config.setAllowCredentials(true);//允许携带cookie

        UrlBasedCorsConfigurationSource source= new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);//对所有经过网关的请求都生效
        return new CorsWebFilter(source);
    }
}
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容