springcloud

http://localhost:8001/customer/30[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zpn6SWDb-1607759535247)(Pictures/1587880250279.png)]

Author:Eric

Version:9.0.0

文章目录

一、SpringCloud介绍


1.1 微服务架构

微服务架构的提出者:马丁福勒

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

简而言之,微服务架构样式[1]是一种将单个应用程序开发为一组小服务的方法,每个小服务都在自己的进程中运行并与轻量级机制(通常是HTTP资源API)进行通信。这些服务围绕业务功能构建,并且可以由全自动部署机制独立部署。这些服务的集中管理几乎没有,它可以用不同的编程语言编写并使用不同的数据存储技术。

1、 微服务架构只是一个样式,一个风格。

2、 将一个完成的项目,拆分成多个模块去分别开发。

3、 每一个模块都是单独的运行在自己的容器中。

4、 每一个模块都是需要相互通讯的。 Http,RPC,MQ。

5、 每一个模块之间是没有依赖关系的,单独的部署。

6、 可以使用多种语言去开发不同的模块。

7、 使用MySQL数据库,Redis,ES去存储数据,也可以使用多个MySQL数据库。

总结:将复杂臃肿的单体应用进行细粒度的划分,每个拆分出来的服务各自打包部署。

1.2 SpringCloud介绍
  • SpringCloud是微服务架构落地的一套技术栈。

  • SpringCloud中的大多数技术都是基于Netflix公司的技术进行二次研发。

  • SpringCloud的中文社区网站:http://springcloud.cn/

  • SpringCloud的中文网:http://springcloud.cc/

  • 八个技术点:

    • Eureka - 服务的注册与发现
    • Robbin - 服务之间的负载均衡
    • Feign - 服务之间的通讯
    • Hystrix - 服务的线程隔离以及断路器
    • Zuul - 服务网关
    • Stream - 实现MQ的使用
    • Config - 动态配置
    • Sleuth - 服务追踪

二、服务的注册与发现-Eureka【重点


2.1 引言

Eureka就是帮助我们维护所有服务的信息,以便服务之间的相互调用

Eureka
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T4B8Cb5e-1607759535255)(Pictures/1587887319057.png)]

SpringBoot 2.2.9

SpringCloud Hoxton.SR4

2.2 Eureka的快速入门
2.2.1 创建一个父工程

创建一个父工程【springcloud-ghy】,并且在父工程中指定SpringCloud的版本,并且将packing修改为pom

除了pom.xml【idea配置,.gitignore】,其他都可以删掉

<packaging>pom</packaging>

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

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR4</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
2.2.2 创建EurekaServer【01-eureka-server】

创建eureka的server【01-eureka-server】

创建Maven工程,改成SpringBoot工程

  1. 导入依赖
    <!-- 1. 导入eureka的服务 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
  2. 使用JBL SpringBootAppGen插件生成引导类和配置文件

启动引导类添加注解 @EnableEurekaServer

@SpringBootApplication
@EnableEurekaServer  //代表当前应用是一个Eureak服务
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}

编写yml配置文件

server:
  port: 10086

spring:
  application:
    name: eureka-server

eureka:
  instance:
    hostname: localhost
  client:
    #当前服务要不要注册到eureka
    registerWithEureka: false
    #拉取信息
    fetchRegistry: false
    serviceUrl:
      #eureka的服务地址
      # http://localhost:10086/eureka/
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

测试

  1. 启动引导类
  2. 浏览器访问 http://localhost:10086,能够看到一个美丽的界面【Eureka】就成功了
2.2.3 创建EurekaClient【02-search-service】

创建Maven工程【02-search-service】,修改为SpringBoot

导入依赖

<!-- 1. 导入eureka的客户端起步依赖-->
<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-web</artifactId>
</dependency>

在启动引导类上添加注解 @EnableEurekaClient

@SpringBootApplication
@EnableEurekaClient  //标记当前服务是Eureka的客户端
public class SearchApplication {
    public static void main(String[] args) {
        SpringApplication.run(SearchApplication.class, args);
    }
}

编写配置文件

server:
  port: 9001

spring:
  application:
    name: search-service

# 指定eurkea服务地址
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:10086/eureka/

编写SearchController

package com.qf.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-09
 **/
@RestController
@RequestMapping("/search")
public class SearchController {

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

    @RequestMapping("/test")
    public String test() {
        return "Hello Test " + port;
    }

}

测试

  1. 先启动 00-eureka-server
  2. 启动引导类
  3. 浏览器刷新 http://localhost:10086,能够看到 search-service
  4. 浏览器访问 http://localhost:9001/search/test,能够看到效果,就代表成功
2.2.4 创建EurekaClient【03-customer-service】

导入依赖

<!-- 1. 导入eureka的客户端起步依赖-->
<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-web</artifactId>
</dependency>

在启动引导类上添加注解 @EnableEurekaClient

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

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

}

编写配置文件

server:
  port: 8001

spring:
  application:
    name: customer-service

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:10086/eureka/

编写SearchController

package com.ghy.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-09
 **/
@RestController
@RequestMapping("/customer")
public class CustomerController {

    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/test")
    public String test(){
        String result = restTemplate.getForObject("http://localhost:9001/search/test", String.class);
        return result;
    }

}

测试

  1. 先启动 01-eureka-server
  2. 启动引导类
  3. 浏览器刷新 http://localhost:10086,能够看到 customer-service
  4. 浏览器访问 http://localhost:9001/customer/test,能够看到效果,就代表成功
2.2.5 使用到EurekaClient的对象去获取服务信息

使用到EurekaClient的对象去获取服务信息

在 03-customer-service 工程的 CustomerController中注入

@Autowired
private EurekaClient eurekaClient;

正常RestTemplate调用即可

@GetMapping("/test")
public String test(){

    InstanceInfo instanceInfo = eurekaClient.getNextServerFromEureka("SEARCH-SERVICE", false);

    String uri = instanceInfo.getHomePageUrl();
    //http://localhost:9001/
    System.out.println("uri-> " + uri);

    String result = restTemplate.getForObject(uri + "/search/test", String.class);
    return result;
}
2.3 Eureka的安全性

01-eureka-server

实现Eureka认证

导入依赖

<!-- eureka安全 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

编写配置类

package com.qf.config;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-10
 **/
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //忽略 /eureka/** 路径
        http.csrf().ignoringAntMatchers("/eureka/**");
        super.configure(http);
    }
}

编写配置文件

# 指定用户名和密码
spring:
  application:
    name: eureka-server
  security:
    user:
      name: root
      password: root

问题:

​ 当启动 Eureka的客户端时,出错了

​ 原因是因为当前Eureka已经开启安全了,那么注册到Eureka的服务也是需要提供认证的【用户名跟密码】

com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server

解决方案:

​ 其他服务想注册到Eureka上需要添加用户名和密码。【只要是注册到Eureka中的服务都需要配置认证信息】

# 指定eurkea服务地址
eureka:
  client:
    serviceUrl:
      #defaultZone: http://localhost:10086/eureka/
      defaultZone: http://root:root@localhost:10086/eureka/

在我们开发过程中,是很少启动认证的,因为每次重启都需要登录,太麻烦。

为了方便写代码,我们去掉认证

01-eureka-server

  1. 注释安全依赖
  2. 把类删掉
  3. 把配置文件中用户名密码相关信息注释掉

02-search-service 跟 03-customer-service

​ 把 defaultZone 改成原来的【不需要用户名跟密码】

当前springcloud应用过多,不方便查看

开启 dashboard

image-20200831172802811

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uSxa2m5l-1607759535258)(Pictures\image-20200910114738335.png)]

2.4 Eureka的高可用

如果程序正在运行,突然Eureka宕机了。

  • 如果调用方访问过一次被调用方了,因为本地缓存了被调用方的信息,Eureka的宕机不会影响到功能。

  • 如果调用方没有访问过被调用方,Eureka的宕机就会造成当前功能不可用。

2.4.1 搭建两个Eureka

修改01-eureka-server的application.yml

server:
  port: ${port:10086}

spring:
  application:
    name: eureka-server


#集群
eureka:
  instance:
    hostname: localhost
  client:
    #如果是集群,则需要相互注册
    rgisterWithEureka: true
    #拉取信息
    fetchRegistry: true
    serviceUrl:
      #需要把自己注册到对方的地址
      defaultZone: ${defaultZone:http://localhost:10087/eureka/}

在 idea 的spring-boot应用配置界面上,复制一个 EureakApplication 启动项,改名为 EureakApplication 10087;并且在面板上启动参数

参考下图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9kz6WaKC-1607759535262)(C:\Users\ghy\AppData\Roaming\Typora\typora-user-images\image-20200910141107891.png)]

2.4.2 修改EurekaClient的application.yml文件,让其注册到两个Eureka中。

customer-service

search-service

# 指定eurkea服务地址
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:10086/eureka/,http://localhost:10087/eureka/
2.4.3 测试

启动 02-search-service,03-customer-service 服务

浏览器上刷新eureka,就能够看到两个Eureka上都有了02-search-service,03-customer-service这两个服务。

如果没有同步,则可以选择重新启两个Eureka,或者等会,Eureka节点需要时间同步数据。

2.5 Eureka的细节

EurekaClient启动是,将自己的信息注册到EurekaServer上,EurekaSever就会存储上EurekaClient的注册信息。

当EurekaClient调用服务时,本地没有注册信息的缓存时,去EurekaServer中去获取注册信息。

EurekaClient会通过心跳的方式去和EurekaServer进行连接。(默认30s EurekaClient会发送一次心跳请求,如果超过了90s还没有发送心跳信息的话,EurekaServer就认为你宕机了,将当前EurekaClient从注册表中移除)

eureka:
  instance:
    lease-renewal-interval-in-seconds: 30      #心跳的间隔
    lease-expiration-duration-in-seconds: 90    # 多久没发送,就认为你宕机了

EurekaClient会每隔30s去EurekaServer中去更新本地的注册表

eureka:
  client:
    registry-fetch-interval-seconds: 30 # 每隔多久去更新一下本地的注册表缓存信息

Eureka的自我保护机制,统计15分钟内,如果一个服务的心跳发送比例低于85%,EurekaServer就会开启自我保护机制

  • 不会从EurekaServer中去移除长时间没有收到心跳的服务。
  • EurekaServer还是可以正常提供服务的。
  • 网络比较稳定时,EurekaServer才会开始将自己的信息被其他节点同步过去

这是触发了Eureka的自我保护机制。当服务未按时进行心跳续约时,Eureka会统计服务实例最近15分钟心跳续约的
比例是否低于了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务
剔除列表并不妥当,因为服务可能没有宕机。Eureka在这段时间内不会剔除任何服务实例,直到网络恢复正常。生
产环境下这很有效,保证了大多数服务依然可用,不过也有可能获取到失败的服务实例,因此服务调用者必须做好服
务的失败容错。

eureka:
  server:
    enable-self-preservation: true  # 开启自我保护机制

*** (面试可能有)CAP定理,C - 一致性,A-可用性,P-分区容错性,这三个特性在分布式环境下,只能满足2个,而且分区容错性在分布式环境下,是必须要满足的,只能在AC之间进行权衡。

如果选择CP,保证了一致性,可能会造成你系统在一定时间内是不可用的,如果你同步数据的时间比较长,造成的损失大。

Eureka就是一个AP的效果,高可用的集群,Eureka集群是无中心,Eureka即便宕机几个也不会影响系统的使用,不需要重新的去推举一个master,也会导致一定时间内数据是不一致。

三、服务间的负载均衡-Robbin【重点


3.1 引言

Robbin是帮助我们实现服务和服务负载均衡,Robbin属于客户端负载均衡

客户端负载均衡:customer客户模块,将2个Search模块信息全部拉取到本地的缓存,在customer中自己做一个负载均衡的策略,选中某一个服务。

服务端负载均衡:在注册中心中,直接根据你指定的负载均衡策略,帮你选中一个指定的服务信息,并返回。

Robbin
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MfGWI0lD-1607759535265)(Pictures/1587977965492.png)]
3.2 Robbin的快速入门

启动两个search模块

​ 复制一个search模块的启动项,启动时加上端口号即可

在customer导入robbin依赖

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

配置整合RestTemplate和Robbin

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

在customer中去访问search

@GetMapping("/test")
public String test(){

    //使用ribbon来实现负载均衡时,需要通过服务名去访问。
    //默认的负载均衡策略为轮询
    String result = restTemplate.getForObject("http://SEARCH-SERVICE/search/test", String.class);

    return result;
}
3.3 Robbin配置负载均衡策略

负载均衡策略

  • RandomRule:随机策略
  • RoundRobbinRule:轮询策略
  • WeightedResponseTimeRule:默认会采用轮询的策略,后续会根据服务的响应时间,自动给你分配权重
  • BestAvailableRule:根据被调用方并发数最小的去分配

采用注解的形式【全局】

@Bean
public IRule robbinRule(){
    return new RandomRule();
}

配置文件去指定负载均衡的策略(推荐)

# 指定具体服务的负载均衡策略
SEARCH-SERVICE:      # 编写服务名称
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule  # 具体负载均衡使用的类

注释IRule对应的配置Bean

/*@Bean
public IRule robbinRule(){
    return new RandomRule();
}*/

四、服务间的调用-Feign【重点


4.1 引言

Feign可以帮助我们实现面向接口编程,就直接调用其他的服务,简化开发。

4.2 Feign的快速入门

导入依赖

<!-- 导入openfeign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

引导类添加一个注解

@EnableFeignClients

创建一个接口,并且和search-service模块做映射

package com.ghy.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-10
 **/
@FeignClient("SEARCH-SERVICE")
public interface SearchFeign {

    /**
     * 将来,这个方法被调用时,会通过动态代理生成代理对象去调用真正目标对象
     * 在映射时,需要把value属性加上
     * @return
     */
    @RequestMapping(value = "/search/test")
    String test();

}

CustomerController

@Autowired
SearchFeign searchFeign;

@GetMapping("/test")
public String test(){
    System.out.println("CustomerController test 。。。。");
    String result = searchFeign.test();
    return result;
}

测试

浏览器访问 http://localhost:8001/customer/test ,能够正确调用到 search-service 模块的Controller接口的具体方法

4.3 Feign的传递参数方式

注意事项

  • 如果你传递的参数,比较复杂时,默认会采用POST的请求方式。
  • 传递单个参数时,推荐使用@PathVariable,如果传递的单个参数比较多,这里也可以采用@RequestParam,不要省略value属性
  • 传递对象信息时,统一采用json的方式,添加@RequestBody
  • Client接口必须采用@RequestMapping

4.3.1 search-service 模块和customer-service模块

引入 lombok依赖

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

search-service 模块和customer-service模块准备Customer实体类

package com.ghy.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-10
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Customer {

    private Integer id;
    private String name;
    private Integer age;

}

在Search模块下准备三个接口

package com.qf.controller;

import com.qf.entity.Customer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-09
 **/
@RestController
@RequestMapping("/search")
public class SearchController {


    //========================================
    //参数的传递
    @GetMapping("/{id}")
    public Customer findById(@PathVariable Integer id) {
        return new Customer(id, "@PathVariable", 1);
    }


    @GetMapping("/get")
    public Customer get(@RequestParam("id") Integer id,
                             @RequestParam("name") String name,
                             @RequestParam("age") Integer age) {
        return new Customer(id, name, 2);
    }

    @PostMapping("/save")
    public Customer save(@RequestBody Customer customer) {
        return customer;
    }
    //========================================

}

在customer-service 模块的feign接口中增加如下方法

package com.ghy.feign;

import com.ghy.entity.Customer;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-10
 **/
@FeignClient("SEARCH-SERVICE")
public interface SearchFeign {


    @GetMapping("/search/{id}")
    Customer findById(@PathVariable Integer id);


    @GetMapping("/search/get")
    Customer get(@RequestParam(value = "id") Integer id,
                        @RequestParam(value = "name") String name,
                        @RequestParam(value = "age") Integer age);

    @PostMapping("/search/save")
    Customer save(@RequestBody Customer customer);

}

在customer-service 模块的Controller中增加调用feign接口中的方法

package com.ghy.controller;

import com.ghy.entity.Customer;
import com.ghy.feign.SearchFeign;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.EurekaClient;
import com.netflix.discovery.converters.Auto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import java.nio.file.FileAlreadyExistsException;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-09
 **/
@RestController
@RequestMapping("/customer")
public class CustomerController {

    @Autowired
    SearchFeign searchFeign;

    //========================================
    //参数的传递
    @GetMapping("/{id}")
    public Customer findById(@PathVariable Integer id) {
        System.out.println("CustomerController findById ....");
        return searchFeign.findById(id);
    }


    @GetMapping("/get")
    public Customer get(@RequestParam("id") Integer id,
                        @RequestParam("name") String name,
                        @RequestParam("age") Integer age) {
        System.out.println("CustomerController get ....");
        return searchFeign.get(id,name,age);
    }

    @PostMapping("/save")
    public Customer save(@RequestBody Customer customer) {
        System.out.println("CustomerController save ....");
        return searchFeign.save(customer);
    }
    //========================================


}

测试

image-20200910162132628
image-20200910162219185
image-20200910162306970
4.4 Feign的Fallback
4.4.0 引言

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wHSaRfkR-1607759535267)(Pictures\image-20200911111247662.png)]

Fallback可以帮助我们在使用Feign去调用另外一个服务时,如果出现了问题,走服务降级,返回一个错误数据,避免功能因为一个服务出现问题,全部失效。

4.4.1 FallBack方式

创建一个POJO类,实现SearchFeign接口,重写其抽象方法,并注入到Spring容器中

package com.ghy.feign.fallback;

import com.ghy.entity.Customer;
import com.ghy.feign.SearchFeign;
import org.springframework.stereotype.Component;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-11
 **/
@Component
public class SearchFeignFallback implements SearchFeign {
    @Override
    public String test() {
        return "feign的服务降级成功了";
    }

    @Override
    public Customer findById(Integer id) {
        return null;
    }

    @Override
    public Customer get(Integer id, String name, Integer age) {
        return null;
    }

    @Override
    public Customer save(Customer customer) {
        return null;
    }
}

修改Feign接口中的注解,添加一个属性。

@FeignClient(value = "SEARCH-SERVICE", fallback = SearchFeignFallback.class)
public interface SearchFeign {}

添加一个配置文件。【开启feign跟hystrix的整合服务降级】

# feign和hystrix组件整合
feign:
  hystrix:
    enabled: true

让搜索模块【search-service】的test方法出现异常【模拟服务出问题了】

@RequestMapping("/test")
public String test() {
    int i=1/0;
    return "Hello Test " + port;
}

测试

访问客户模块的test接口 http://localhost:8001/customer/test

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HggJmAz0-1607759535269)(Pictures\image-20200911105853736.png)]

4.4.2 FallBackFactory方式

调用方无法知道具体的错误信息是什么,通过FallBackFactory的方式去实现这个功能

FallBackFactory基于Fallback

创建一个POJO类,实现FallBackFactory<Feign接口实现类>,注入到Spring容器中,抽象方法中需要把Feign接口服务降级类对象返回

package com.ghy.feign.fallback.factory;

import com.ghy.feign.fallback.SearchFeignFallback;
import com.netflix.discovery.converters.Auto;
import feign.hystrix.FallbackFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author ghy
 * @version 1.0
 * @date 2020-09-11
 **/
@Component
public class SearchFeignFallbackFactory implements FallbackFactory<SearchFeignFallback> {

    @Autowired
    SearchFeignFallback searchFeignFallback;

    /**
     * 获取目标接口的问题信息
     * @param throwable
     * @return
     */
    @Override
    public SearchFeignFallback create(Throwable throwable) {
        //输出异常堆栈信息
        throwable.printStackTrace();
        return searchFeignFallback;
    }
}

修改SearchFeign接口中的属性

@FeignClient(value = "SEARCH-SERVICE", fallbackFactory= SearchFeignFallbackFactory.class)

测试

访问客户模块的test接口 http://localhost:8001/customer/test

​ 页面依旧是降级后结果,customer-service应用的控制台输出了异常信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JdJ5MziK-1607759535271)(Pictures\image-20200911110943297.png)]

五、服务的隔离及断路器-Hystrix【重点


5.1 引言
Hystrix
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KbFAGLZL-1607759535274)(Pictures/1588044427589.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JwWWSQA0-1607759535275)(Pictures\image-20200911112725280.png)]

5.2 降级机制实现

导入依赖

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

引导类添加hystrix的一个注解

@EnableCircuitBreaker

让findById接口出问题

编写他的降级方法【除方法名之外,其他都跟目标方法一致】

在目标方法中加上注解,实现服务降级【@HystrixCommand(fallbackMethod = “findByIdFallback”)】

@GetMapping("/{id}")
//当前降级服务的方法必须跟目标方法保持一致,除方法名之外
@HystrixCommand(fallbackMethod = "findByIdFallback")
public Customer findById(@PathVariable Integer id) {
    System.out.println("CustomerController findById ....");
    int i=1/0;   //出问题了,希望走降级逻辑
    return searchFeign.findById(id);
}

//服务降级的方法
public Customer findByIdFallback(Integer id) {
    System.out.println("findById 出错了,进入了降级逻辑....");
    return new Customer(id, "hystrix服务降级成功",666);
}

5、 测试一下

测试编写了降级逻辑的接口【findById】

http://localhost:8001/customer/30

效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tFQZNiEr-1607759535276)(Pictures\image-20200911113704966.png)]
5.3 线程隔离

如果使用Tomcat的线程池去接收用户的请求,使用当前线程去执行其他服务的功能,如果某一个服务出现了故障,导致tomcat的线程大量的堆积,导致Tomcat无法处理其他业务功能。

  • Hystrix的线程池(默认),接收用户请求采用tomcat的线程池,执行业务代码,调用其他服务时,采用Hystrix的线程池。
  • 信号量,使用的还是Tomcat的线程池,帮助我们去管理Tomcat的线程池。

Hystrix的线程池的配置

配置信息namevalue
线程隔离策略execution.isolation.strategyTHREAD
指定超时时间execution.isolation.thread.timeoutInMilliseconds1000
是否开启超时时间配置execution.timeout.enabledtrue
超时之后是否中断线程execution.isolation.thread.interruptOnTimeouttrue
取消任务后是否中断线程execution.isolation.thread.interruptOnCancelfalse

代码实现

@HystrixProperty 具体的属性参考 HystrixCommandProperties类中的属性

//---------------------------
@GetMapping("/get")
@HystrixCommand(fallbackMethod = "getFallback",commandProperties = {
        //name : Hystrix线程池配置的属性名
        //value : 属性值
        @HystrixProperty(name="execution.isolation.strategy", value = "THREAD"),
        @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds", value = "2000"),
        @HystrixProperty(name="execution.timeout.enabled", value = "true"),
})
public Customer get(@RequestParam("id") Integer id,
                    @RequestParam("name") String name,
                    @RequestParam("age") Integer age) {

    try {
        System.out.println("CustomerController get ....");
        System.out.println("线程: " + Thread.currentThread().getName());
        Thread.sleep(2500);//目的是为了让当前方法超时,进入降级逻辑
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return searchFeign.get(id,name,age);
}

public Customer getFallback(Integer id,String name,Integer age) {
    System.out.println("getFallback被降级了。是线程隔离Hystrix线程池的方式进行服务降级....");
    return new Customer(id, "Hystrix线程池服务降级成功",666);
}

//---------------------------

测试 http://localhost:8001/customer/get?id=1&name=111&age=11111

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ONfwXBpJ-1607759535278)(Pictures\image-20200911120251608.png)]

信号量的配置信息

配置信息namevalue
线程隔离策略execution.isolation.strategySEMAPHORE
指定信号量的最大并发请求数execution.isolation.semaphore.maxConcurrentRequests10

代码实现

//--------------- 信号量来管理tomcat线程 ----------------
@PostMapping("/save")
@HystrixCommand(fallbackMethod = "saveFallback",commandProperties = {
        //name : Hystrix线程池配置的属性名
        //value : 属性值
        @HystrixProperty(name="execution.isolation.strategy", value = "SEMAPHORE"),
        @HystrixProperty(name="execution.isolation.semaphore.maxConcurrentRequests", value = "10")
})
public Customer save(@RequestBody Customer customer) {
    System.out.println("CustomerController save ....");
    System.out.println("线程 ---> " + Thread.currentThread().getName());
    int i=1/0;
    return searchFeign.save(customer);
}
//========================================

public Customer saveFallback(Customer customer){
    System.out.println("这是信号量配置,出现异常,进入降级逻辑...............");
    return new Customer(customer.getId(),"信号量配置,出现异常,进入降级逻辑", 5555);
}

测试

​ 因为是post请求,所以使用 postman测试

​ 重启 customer-service

​ 测试接口: http://localhost:8001/customer/save

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xbgABPqY-1607759535279)(Pictures\image-20200911162158974.png)]

控制台能够看到的确使用的是tomcat的线程

image-20200911162423707
5.4 断路器

也叫熔断器

5.4.1 断路器介绍

马丁福勒断路器论文:https://martinfowler.com/bliki/CircuitBreaker.html

在调用指定服务时,如果说这个服务的失败率达到你输入的一个阈值,将断路器从closed状态,转变为open状态,指定服务时无法被访问的,如果你访问就直接走fallback方法,在一定的时间内,open状态会再次转变为half open状态,允许一个请求发送到我的指定服务,如果成功,转变为closed,如果失败,服务再次转变为open状态,会再次循环到half open,直到断路器回到一个closed状态。

断路器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GdgP93wo-1607759535280)(Pictures\image-20200911163313991.png)]
5.4.2 配置断路器的监控界面

导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>

在启动引导类中添加注解

@EnableHystrixDashboard

配置一个Servlet路径,指定上Hystrix的Servlet

@WebServlet("/hystrix.stream")
public class HystrixServlet extends HystrixMetricsStreamServlet {
}

//------------------------------------------------------------
// 在启动类上,添加扫描Servlet的注解
@ServletComponentScan("com.ghy.servlet")

测试直接访问 http://host:port/hystrix

http://localhost:8001/hystrix

​ 进入之后,输入被监控的地址 http://localhost:8001/hystrix.stream

监控界面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NWGuvLh5-1607759535281)(Pictures\image-20200911172103719.png)]

在当前位置输入映射好的servlet路径

检测效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y5PwBpq5-1607759535282)(Pictures\image-20200911172012892.png)]
5.4.3 配置断路器的属性

断路器的属性(默认10s秒中之内请求数)

配置信息namevalue
断路器的开关circuitBreaker.enabledtrue
失败阈值的总请求数circuitBreaker.requestVolumeThreshold20
请求总数失败率达到%多少时circuitBreaker.errorThresholdPercentage50
断路器open状态后,多少秒是拒绝请求的circuitBreaker.sleepWindowInMilliseconds5000
强制让服务拒绝请求 [A]circuitBreaker.forceOpenfalse
强制让服务接收请求 [B]circuitBreaker.forceClosedfalse

[A] 跟 [B] 只能选一个

具体配置方式,参数参考 HystrixCommandProperties类进行配置

@GetMapping("/hello/{id}")
@HystrixCommand(fallbackMethod = "helloFallBack",commandProperties = {
        @HystrixProperty(name = "circuitBreaker.enabled",value = "true"),  //是否打开断路器,默认为true:打开
        @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),  //总共在10秒访问多少次,默认为20次
        @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "50"),  //失败的比例达到多少【百分比】打开断路器
        @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "5000") //过了多少时间后,断路器变成半开状态
})
public String hello(@PathVariable Integer id){
    System.out.println("customer hello");
    if(id == 1) {
        int i=1/0;   //访问至少20次,有一半以上是失败的,则打开断路器
    }
    return "hello,正常了 " + id;
}

public String helloFallBack(Integer id){
    System.out.println("hello方法出异常了,进入降级逻辑");
    return id.toString() + "-> id=1,出异常,其他正常。。。。。";
}@GetMapping("/customer/{id}")
@HystrixCommand(fallbackMethod = "findByIdFallBack",commandProperties = {
    @HystrixProperty(name = "circuitBreaker.enabled",value = "true"),
    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),
    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "70"),
    @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "5000")
})

在目标方法中,通过id的值去判断当前请求是否成功

当id==1时,请求失败,当失败比例达到阈值时,打开断路器,这个时候,就算访问成功的请求,也会失败。等到打开断路器的时间到了这后,会允许一个请求访问,如果成功,则关闭断路器,如果失败,依旧打开断路器。之后循环以上操作

if(id == 1) {
    int i=1/0;
}

测试

  1. 先访问一个正常的接口 http://localhost:8001/customer/hello/2, 返回正常

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dMQpZp7u-1607759535284)(Pictures\image-20200911172440795.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZBhcxTa0-1607759535285)(Pictures\image-20200911172622552.png)]

  1. 再在10秒钟之内,访问失败的接口10次【http://localhost:8001/customer/hello/1】,看到监控界面中,断路器开关被打开了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-369jI13Q-1607759535287)(Pictures\image-20200911172749166.png)]

  1. 这个时候,即便的访问的是正常的接口,也会失败。即 http://localhost:8001/customer/hello/2,也出现了服务降级

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M4ephX4s-1607759535288)(Pictures\image-20200911172833743.png)]

5.5 请求缓存
5.5.1 请求缓存介绍
  • 请求缓存的声明周期是一次请求

  • 请求缓存是缓存当前线程中的一个方法,将方法参数作为key,方法的返回结果作为value

  • 在一次请求中,目标方法被调用过一次,以后就都会被缓存。

请求缓存
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U06lwvqi-1607759535291)(Pictures/1588127678866.png)]
5.5.2 请求缓存的实现

创建一个Service,在Service中调用Search服务。

@Service
public class CustomerService {

    @Autowired
    private SearchClient searchClient;


    @CacheResult
    @HystrixCommand(commandKey = "findById")
    public Customer findById(@CacheKey Integer id) throws InterruptedException {
        return searchClient.findById(id);
    }

    @CacheRemove(commandKey = "findById")
    @HystrixCommand
    public void clearFindById(@CacheKey Integer id){
        System.out.println("findById被清空");
    }

}

使用请求缓存的注解

@CacheResult:帮助我们缓存当前方法的返回结果(必须@HystrixCommand配合使用)
@CacheRemove:帮助我们清除某一个缓存信息(基于commandKey)
@CacheKey:指定哪个方法参数作为缓存的标识

修改Search模块的返回结果

#使用随机值来证明会有缓存
return new Customer(1,"张三",(int)(Math.random() * 100000));

编写Filter,去构建HystrixRequestContext

@WebFilter("/*")
public class HystrixRequestContextInitFilter implements Filter {

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HystrixRequestContext.initializeContext();
        filterChain.doFilter(servletRequest,servletResponse);
    }
}

修改Controller

public Customer findById(@PathVariable Integer id) throws InterruptedException {
    System.out.println(customerService.findById(id));
    System.out.println(customerService.findById(id));
    customerService.clearFindById(id);
    System.out.println(customerService.findById(id));
    System.out.println(customerService.findById(id));
    return searchClient.findById(id);
}

测试结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fd85DdUY-1607759535292)(Pictures/1588133189886.png)]

六、服务的网关-Zuul【重点


6.1 引言
  • 客户端维护大量的ip和port信息,直接访问指定服务

  • 认证和授权操作,需要在每一个模块中都添加认证和授权的操作

  • 项目的迭代,服务要拆分,服务要合并,需要客户端进行大量的变化

  • 统一的把安全性校验都放在Zuul中

zuul
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qUEnaHEN-1607759535293)(Pictures/1588145514669.png)]
6.2 Zuul的快速入门

创建Maven项目,修改为SpringBoot

导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

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

添加一个注解

@EnableEurekaClient
@EnableZuulProxy

编写配置文件

# 指定Eureka服务地址
eureka:
  client:
    service-url:
      defaultZone: http://root:root@localhost:8761/eureka,http://root:root@localhost:8762/eureka

#指定服务的名称
spring:
  application:
    name: ZUUL

server:
  port: 80

直接测试

测试效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HTzbYo2U-1607759535295)(Pictures/1588148029538.png)]
6.3 Zuul常用配置信息
6.3.1 Zuul的监控界面

导入依赖

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

编写配置文件

# 查看zuul的监控界面(开发时,配置为*,上线,不要配置)
management:
  endpoints:
    web:
      exposure:
        include: "*"

直接访问

/actuator/routes

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fYNGOTlB-1607759535297)(Pictures/1588157803975.png)]

6.3.2 忽略服务配置
# zuul的配置
zuul:
  # 基于服务名忽略服务,无法查看 ,如果要忽略全部的服务  "*",默认配置的全部路径都会被忽略掉(自定义服务的配置,无法忽略的)
  ignored-services: eureka
  # 监控界面依然可以查看,在访问的时候,404
  ignored-patterns: /**/search/**
6.3.3 自定义服务配置
# zuul的配置
zuul:
  # 指定自定义服务(方式一 , key(服务名):value(路径))
#  routes:
#    search: /ss/**
#    customer: /cc/**
  # 指定自定义服务(方式二)
  routes:
    kehu:   # 自定义名称
      path: /ccc/**     # 映射的路径
      serviceId: customer   # 服务名称
6.3.4 灰度发布

添加一个配置类

@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
    return new PatternServiceRouteMapper(
        "(?<name>^.+)-(?<version>v.+$)",
        "${version}/${name}");
}

准备一个服务,提供2个版本

version: v1

#指定服务的名称
spring:
  application:
    name: CUSTOMER-${version}

修改Zuul的配置

# zuul的配置
zuul:
  # 基于服务名忽略服务,无法查看  , 如果需要用到-v的方式,一定要忽略掉
  # ignored-services: "*"

测试

测试效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G0vspNUN-1607759535298)(Pictures/1588165064217.png)]
6.4 Zuul的过滤器执行流程

客户端请求发送到Zuul服务上,首先通过PreFilter链,如果正常放行,会吧请求再次转发给RoutingFilter,请求转发到一个指定的服务,在指定的服务响应一个结果之后,再次走一个PostFilter的过滤器链,最终再将响应信息交给客户端。

过滤器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CkC33rjN-1607759535300)(Pictures/1588172828199.png)]
6.5 Zuul过滤器入门

创建POJO类,继承ZuulFilter抽象类

@Component
public class TestZuulFilter extends ZuulFilter {}

指定当前过滤器的类型

@Override
public String filterType() {
    return FilterConstants.PRE_TYPE;
}

指定过滤器的执行顺序

@Override
public int filterOrder() {
    return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
}

配置是否启用

@Override
public boolean shouldFilter() {
    // 开启当前过滤器
    return true;
}

指定过滤器中的具体业务代码

@Override
public Object run() throws ZuulException {
    System.out.println("prefix过滤器执行~~~");
    return null;
}

测试

效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ScMK8UuG-1607759535301)(Pictures/1588175034889.png)]
6.6 PreFilter实现token校验

准备访问路径,请求参数传递token

http://localhost/v2/customer/version?token=123

创建AuthenticationFilter

@Component
public class AuthenticationFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return PRE_DECORATION_FILTER_ORDER - 2;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        //..
    }
    
}

在run方法中编写具体的业务逻辑代码

@Override
public Object run() throws ZuulException {
    //1. 获取Request对象
    RequestContext requestContext = RequestContext.getCurrentContext();
    HttpServletRequest request = requestContext.getRequest();

    //2. 获取token参数
    String token = request.getParameter("token");

    //3. 对比token
    if(token == null || !"123".equalsIgnoreCase(token)) {
        //4. token校验失败,直接响应数据
        requestContext.setSendZuulResponse(false);
        requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
    }
    return null;
}

测试

效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9DsUZV63-1607759535303)(Pictures/1588177367016.png)]
6.7 Zuul的降级

创建POJO类,实现接口FallbackProvider

@Component
public class ZuulFallBack implements FallbackProvider {}

重写两个方法

@Override
public String getRoute() {
    return "*";   // 代表指定全部出现问题的服务,都走这个降级方法
}

@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
    System.out.println("降级的服务:" + route);
    cause.printStackTrace();

    return new ClientHttpResponse() {
        @Override
        public HttpStatus getStatusCode() throws IOException {
            // 指定具体的HttpStatus
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }

        @Override
        public int getRawStatusCode() throws IOException {
            // 返回的状态码
            return HttpStatus.INTERNAL_SERVER_ERROR.value();
        }

        @Override
        public String getStatusText() throws IOException {
            // 指定错误信息
            return HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase();
        }

        @Override
        public void close() {

        }

        @Override
        public InputStream getBody() throws IOException {
            // 给用户响应的信息
            String msg = "当前服务:" + route + "出现问题!!!";
            return new ByteArrayInputStream(msg.getBytes());
        }

        @Override
        public HttpHeaders getHeaders() {
            // 指定响应头信息
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            return headers;
        }
    };
}

测试

效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sYJvYFT5-1607759535305)(Pictures/1588180538336.png)]
6.8 Zuul动态路由

创建一个过滤器

//  执行顺序最好放在Pre过滤器的最后面

在run方法中编写业务逻辑

@Override
public Object run() throws ZuulException {
    //1. 获取Request对象
    RequestContext context = RequestContext.getCurrentContext();
    HttpServletRequest request = context.getRequest();

    //2. 获取参数,redisKey
    String redisKey = request.getParameter("redisKey");

    //3. 直接判断
    if(redisKey != null && redisKey.equalsIgnoreCase("customer")){
        // http://localhost:8080/customer
        context.put(FilterConstants.SERVICE_ID_KEY,"customer-v1");
        context.put(FilterConstants.REQUEST_URI_KEY,"/customer");
    }else if(redisKey != null && redisKey.equalsIgnoreCase("search")){
        // http://localhost:8081/search/1
        context.put(FilterConstants.SERVICE_ID_KEY,"search");
        context.put(FilterConstants.REQUEST_URI_KEY,"/search/1");
    }

    return null;
}

测试

效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZQpoY7I5-1607759535307)(Pictures/1588185307172.png)]

七、多语言支持-Sidecar


7.1 引言

在SpringCloud的项目中,需要接入一些非Java的程序,第三方接口,无法接入eureka,hystrix,feign等等组件。启动一个代理的微服务,代理微服务去和非Java的程序或第三方接口交流,通过代理的微服务去计入SpringCloud的相关组件。

sidecar
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D0ySa1pK-1607759535308)(Pictures/1588234895867.png)]
7.2 Sidecar实现

创建一个第三方的服务

创建一个SpringBoot工程,并且添加一个Controller

创建maven工程【sidecar】,修改为SpringBoot

导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-netflix-sidecar</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

增加引导类跟配置文件

引导类上添加注解

@EnableSidecar

编写配置文件

server:
  port: 81

# 指定Eureka服务地址
eureka:
  client:
    service-url:
      defaultZone: http://root:root@localhost:8761/eureka,http://root:root@localhost:8762/eureka
# 指定服务名称
spring:
  application:
    name: other-service


# 指定代理的第三方服务
sidecar:
  port: 7001

6、 通过customer通过feign调用第三方服务

效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MLiQOLhC-1607759535322)(Pictures/1588237130846.png)]

八、服务间消息传递-Stream


8.1 引言

Stream就是在消息队列的基础上,对其进行封装,让咱们更方便的去操作MQ消息队列。

效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oeJBHLoo-1607759535324)(Pictures/1588245420362.png)]
8.2 Stream快速入门

启动RabbitMQ

消费者【search】-导入依赖

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

消费者-配置文件

spring:
  # 连接RabbitMQ
  rabbitmq:
    host: 192.168.199.109
    port: 5672
    username: test
    password: test
    virtual-host: /test

消费者-监听的队列

public interface StreamClient {
    @Input("myMessage")
    SubscribableChannel input();
}
//-------------------------------------------------
@Component
@EnableBinding(StreamClient.class)
public class StreamReceiver {
    @StreamListener("myMessage")
    public void msg(Object msg){
        System.out.println("接收到消息: " + msg);
    }
}

生产者-导入依赖

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

生产者-配置文件

spring:
  # 连接RabbitMQ
  rabbitmq:
    host: 192.168.199.109
    port: 5672
    username: test
    password: test
    virtual-host: /test

生产者-发布消息

public interface StreamClient {
    @Output("myMessage")
    MessageChannel output();
}
//----------------------------------------------  在启动类中添加注解 @EnableBinding(StreamClient.class)
@Autowired
private StreamClient streamClient;

@GetMapping("/send")
public String send(){
    streamClient.output().send(MessageBuilder.withPayload("Hello Stream!!").build());
    return "消息发送成功!!";
}
8.3 Stream重复消费问题

消费者工程中需要添加一个配置,指定消费者组

spring:
  cloud:
    stream:
      bindings:
        myMessage:				# 队列名称
          group: customer      # 消费者组
8.4 Stream的消费者手动ack

编写配置

spring:
  cloud:
    stream:
      # 实现手动ACK
      rabbit:
        bindings:
          myMessage:
            consumer:
              acknowledgeMode: MANUAL

修改消费端方法

@StreamListener("myMessage")
public void msg(Object msg,
                @Header(name = AmqpHeaders.CHANNEL) Channel channel,
                @Header(name = AmqpHeaders.DELIVERY_TAG) Long deliveryTag) throws IOException {
    System.out.println("接收到消息: " + msg);
    channel.basicAck(deliveryTag,false);
}

九、服务的动态配置-Config【重点


9.1 引言
  • 配置文件分散在不同的项目中,不方便维护。

  • 配置文件的安全问题。

  • 修改完配置文件,无法立即生效。

config
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dkZfOAfL-1607759535326)(Pictures/1588257620824.png)]

准备工作

1、gitee 创建一个空仓库【名称:cloud-config】
2、克隆仓库到本地
3、复制 customer-demo工程中的 application.yml 配置文件到仓库中,改名为 customer-dev.yml
4、提交到本地,推送到远程
9.2 搭建Config-Server

创建Maven工程,修改为SpringBoot

导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

引导类上添加注解

@EnableConfigServer

编写配置文件(Git的操作)

server:
  port: 10000

#config服务也需要注册到eureka中
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka

spring:
  application:
    name: config-demo  #服务名
  cloud:
    config:
      server:
        git:
          basedir: D:\basedir   #本地仓库路径
          uri: https://gitee.com/cqwiu/cloud-config.git   #远程仓库url
          username: 你的用户名		#gitee用户名
          password: 你的密码    	#gitee密码

测试(http://localhost:port/{label}/{application}-{profile}.yml)

http://localhost:10000/master/customer-dev.yml

效果
image-20200903211910531
9.3 搭建Config-Client

就是你想要把配置交给gitee管理的具体服务,在这里,我们以customer-demo为例

导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-client</artifactId>
</dependency>

编写配置文件

# 指定Eureka服务地址
eureka:
  client:
    service-url:
      defaultZone: http://root:root@localhost:8761/eureka,http://root:root@localhost:8762/eureka

#指定服务的名称
spring:
  application:
    name: CUSTOMER-${version}
  cloud:
    config:
      discovery:
        enabled: true
        service-id: CONFIG
      profile: dev

version: v1


# CONFIG -> CUSTOMER-v1-dev.yml

修改配置名称

修改为bootstrap.yml

编辑远程仓库的配置文件 customer-dev.yml

内容如下:【其实就是把工程中存在的配置从gitee上的配置文件中去掉】

eureka:
  instance:
    lease-renewal-interval-in-seconds: 30      #心跳的间隔
    lease-expiration-duration-in-seconds: 90    # 多久没发送,就认为你宕机了

server:
  port: 8001
  
spring:
  # 连接RabbitMQ
  rabbitmq:
    host: 192.168.20.128
    port: 5672
    username: demo
    password: demo
    virtual-host: /demo

SEARCH-DEMO:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule


feign:
  hystrix:
    enabled: true

测试发布消息到RabbitMQ

效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-olStmM87-1607759535327)(Pictures/1588263245773.png)]
9.4 实现动态配置
9.4.1 实现原理
实现原理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MZtl1D2q-1607759535329)(Pictures/1588268131944.png)]
9.4.2 config-demo服务连接RabbitMQ

config-demo跟customer都导入依赖

<!-- 手动刷新向 MQ发送消息 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- 消息总线依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

编写配置文件连接RabbitMQ信息

spring:
  rabbitmq:
    host: 192.168.20.128
    port: 5672
    username: demo
    password: demo
    virtual-host: /demo
9.4.3 实现手动刷新

编写配置文件

management:
  endpoints:
    web:
      exposure:
        include: "*"  #开启bus功能

为customer添加一个controller

@RestController
@RefreshScope   //允许刷新
public class CustomerController {

    @Value("${env}")
    private String env;

    @GetMapping("/env")
    public String env(){
        return env;
    }
}

测试

1. CONFIG在Gitee修改之后,自动拉取最新的配置信息。
2. 其他模块需要更新的话,手动发送一个post请求 http://ip:port/actuator/bus-refresh,不重启项目,即可获取最新的配置信息
http://localhost:10000/actuator/bus-refresh
9.5.4 内网穿透

内网穿透的官网https://natapp.cn/

注册登录

实名认证

购买一个免费的隧道。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i2QvDvse-1607759535330)(Pictures\image-20200903221649035.png)]


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ttJwPcnR-1607759535332)(Pictures\image-20200903221747079.png)]


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rzXrMZ1O-1607759535334)(Pictures\image-20200903221940195.png)]

配置隧道
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hSH2kswU-1607759535335)(Pictures/1588317871876.png)]

下载客户端,并复制config.ini文件,在文件中指定authtoken

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BjCzgOyJ-1607759535336)(Pictures\image-20200903222132643.png)]


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CBWZFeAT-1607759535338)(Pictures\image-20200903222317525.png)]


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wihiktHB-1607759535339)(Pictures\image-20200903222413334.png)]


netapp软件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OORgI9yL-1607759535341)(Pictures/1588317944258.png)]

启动exe文件,并测试使用域名访问config接口

效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JRer0Fpr-1607759535342)(Pictures/1588317929937.png)]
9.5.5 实现自动刷新

配置Gitee中的WebHooks

配置Gitee中的WebHooks
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hdmj5maz-1607759535343)(Pictures\image-20200903222939794.png)]
image-20200903223122971
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DfZqMzOj-1607759535344)(Pictures\image-20200903223203214.png)]

给Config添加一个过滤器

直接去代码中找到Filter
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;

/**
 * create by ghy
 */
@WebFilter("/*")
public class UrlFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest)request;
        HttpServletResponse httpServletResponse = (HttpServletResponse)response;

        String url = new String(httpServletRequest.getRequestURI());

        //只过滤/actuator/bus-refresh请求
        if (!url.endsWith("/actuator/bus-refresh")) {
            chain.doFilter(request, response);
            return;
        }

        //获取原始的body
        String body = readAsChars(httpServletRequest);

        System.out.println("original body:   "+ body);

        //使用HttpServletRequest包装原始请求达到修改post请求中body内容的目的
        CustometRequestWrapper requestWrapper = new CustometRequestWrapper(httpServletRequest);

        chain.doFilter(requestWrapper, response);

    }

    @Override
    public void destroy() {

    }

    private class CustometRequestWrapper extends HttpServletRequestWrapper {
        public CustometRequestWrapper(HttpServletRequest request) {
            super(request);
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            byte[] bytes = new byte[0];
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);

            return new ServletInputStream() {
                @Override
                public boolean isFinished() {
                    return byteArrayInputStream.read() == -1 ? true:false;
                }

                @Override
                public boolean isReady() {
                    return false;
                }

                @Override
                public void setReadListener(ReadListener readListener) {

                }

                @Override
                public int read() throws IOException {
                    return byteArrayInputStream.read();
                }
            };
        }
    }

    public static String readAsChars(HttpServletRequest request)
    {

        BufferedReader br = null;
        StringBuilder sb = new StringBuilder("");
        try
        {
            br = request.getReader();
            String str;
            while ((str = br.readLine()) != null)
            {
                sb.append(str);
            }
            br.close();
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        finally
        {
            if (null != br)
            {
                try
                {
                    br.close();
                }
                catch (IOException e)
                {
                    e.printStackTrace();
                }
            }
        }
        return sb.toString();
    }
}


在引导类上标记 @ServletComponentScan(basePackages = “filter所在包名”)

测试

重启config和customer工程

修改仓库中配置文件的内容

测试
image-20200903224332863

十、服务的追踪-Sleuth【重点


10.1 引言

在整个微服务架构中,微服务很多,一个请求可能需要调用很多很多的服务,最终才能完成一个功能,如果说,整个功能出现了问题,在这么多的服务中,如何去定位到问题的所在点,出现问题的原因是什么。

  • Sleuth可以获得到整个服务链路的信息。

  • Zipkin通过图形化界面去看到信息。

  • Sleuth将日志信息存储到数据库中。

Sleuth
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NTBbLvIO-1607759535346)(Pictures/1588325099243.png)]
10.2 Sleuth的使用

导入依赖

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

编写配置文件

logging:
  level:
    org.springframework.web.servlet.DispatcherServlet: DEBUG

测试

日志信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OiAFMqQy-1607759535347)(Pictures/1588336398785.png)]
SEARCH:服务名称
e9c:总链路id
f07:当前服务的链路id
false:不会将当前的日志信息,输出其他系统中
10.3 Zipkin的使用

搭建Zipkin的web工程 https://zipkin.io/

version: "3.1"
services:
  zipkin:
   image: daocloud.io/daocloud/zipkin:latest
   restart: always
   container_name: zipkin
   ports:
     - 9411:9411

导入依赖:把 sleuth 起步依赖换成 zipkin起步依赖

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

编写配置文件

#指定服务的名称
spring:
  sleuth:
    sampler:
      probability: 1   # 百分之多少的sleuth信息需要输出到zipkin中,1代表百分百
  zipkin:
    base-url: http://192.168.20.128:9411/  # 指定zipkin的地址

测试

测试
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f8auGw37-1607759535348)(Pictures/1588341969002.png)]
10.4 整合RabbitMQ

导入RabbitMQ依赖

customer修改配置文件

spring:
  zipkin:
    sender:
      type: rabbit

修改Zipkin的信息

version: "3.1"
services:
  zipkin:
   image: daocloud.io/daocloud/zipkin:latest
   restart: always
   container_name: zipkin
   ports:
     - 9411:9411
   environment:
     - RABBIT_ADDRESSES=192.168.20.128:5672
     - RABBIT_USER=test
     - RABBIT_PASSWORD=test
     - RABBIT_VIRTUAL_HOST=/test

测试

重启customer

测试
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xtwUkGZ1-1607759535349)(Pictures/1588348586217.png)]
10.5 Zipkin存储数据到ES

重新修改zipkin的yml文件

version: "3.1"
services:
  zipkin:
   image: daocloud.io/daocloud/zipkin:latest
   restart: always
   container_name: zipkin
   ports:
     - 9411:9411
   environment:
     - RABBIT_ADDRESSES=192.168.199.109:5672
     - RABBIT_USER=test
     - RABBIT_PASSWORD=test
     - RABBIT_VIRTUAL_HOST=/test
     - STORAGE_TYPE=elasticsearch
     - ES_HOSTS=http://192.168.199.109:9200

十一、完整SpringCloud架构图【重点


完整架构图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZQ0nH7t4-1607759535351)(Pictures/1588351313922.png)]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用 JavaScript 编写的记忆游戏(附源代码)   项目:JavaScript 记忆游戏(附源代码) 记忆检查游戏是一个使用 HTML5、CSS 和 JavaScript 开发的简单项目。这个游戏是关于测试你的短期 记忆技能。玩这个游戏 时,一系列图像会出现在一个盒子形状的区域中 。玩家必须找到两个相同的图像并单击它们以使它们消失。 如何运行游戏? 记忆游戏项目仅包含 HTML、CSS 和 JavaScript。谈到此游戏的功能,用户必须单击两个相同的图像才能使它们消失。 点击卡片或按下键盘键,通过 2 乘 2 旋转来重建鸟儿对,并发现隐藏在下面的图像! 如果翻开的牌面相同(一对),您就赢了,并且该对牌将从游戏中消失! 否则,卡片会自动翻面朝下,您需要重新尝试! 该游戏包含大量的 javascript 以确保游戏正常运行。 如何运行该项目? 要运行此游戏,您不需要任何类型的本地服务器,但需要浏览器。我们建议您使用现代浏览器,如 Google Chrome 和 Mozilla Firefox, 以获得更好、更优化的游戏体验。要玩游戏,首先,通过单击 memorygame-index.html 文件在浏览器中打开游戏。 演示: 该项目为国外大神项目,可以作为毕业设计的项目,也可以作为大作业项目,不用担心代码重复,设计重复等,如果需要对项目进行修改,需要具备一定基础知识。 注意:如果装有360等杀毒软件,可能会出现误报的情况,源码本身并无病毒,使用源码时可以关闭360,或者添加信任。
使用 JavaScript 编写的 Squareshooter 游戏及其源代码   项目:使用 JavaScript 编写的 Squareshooter 游戏(附源代码) 这款游戏是双人游戏。这是一款使用 JavaScript 编写的射击游戏,带有门户和强化道具。在这里,每个玩家都必须控制方形盒子(作为射手)。这款射击游戏的主要目标是射击对手玩家以求生存。当它射击对手时,它会获得一分。 游戏制作 该游戏仅使用 HTML 和 JavaScript 开发。该游戏的 PC 控制也很简单。 对于玩家 1: T:朝你上次动作的方向射击 A:向左移动 D:向右移动 W:向上移动 S:向下移动 对于玩家2: L:朝你上次移动的方向射击 左箭头:向左移动 右箭头:向右移动 向上箭头:向上移动 向下箭头:向下移动 游戏会一直进行,直到您成功射击对手或对手射击您为止。游戏得分显示在顶部。所有游戏功能均由 JavaScript 设置,而布局和其他次要功能则由 HTML 设置。 如何运行该项目? 要运行此项目,您不需要任何类型的本地服务器,但需要浏览器。我们建议您使用现代浏览器,如 Google Chrome 和 Mozilla Firefox。要运行此游戏,首先,通过单击 index.html 文件在浏览器中打开项目。 演示: 该项目为国外大神项目,可以作为毕业设计的项目,也可以作为大作业项目,不用担心代码重复,设计重复等,如果需要对项目进行修改,需要具备一定基础知识。 注意:如果装有360等杀毒软件,可能会出现误报的情况,源码本身并无病毒,使用源码时可以关闭360,或者添加信任。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值