springcloud/README.md
2020-11-13 17:07:26 +08:00

50 KiB
Raw Permalink Blame History

微服务框架

[toc]

一、SpringCloud介绍

1.1 微服务架构

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

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

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

1.2 SpringCloud介绍

SpringCloud是微服务架构落地的一套技术栈

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

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

2、SpringCloud的中文网 http://springcloud.cc/

八个技术点:

1、Eureka - 服务的注册与发现

2、Robbn - 服务之间的负载均衡

3、Feign - 服务之间的通讯

4、Hystrix - 服务的线程隔离及其熔断器

5、Zuul - 服务网关

6、Stream - 实现MQ的使用

7、Config - 动态配置

8、 Sleuth - 服务追踪

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

2.1 引言

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

1605019839175

2.2 Eureka的快速入门

2.2.1 创建EurekaServer

1、创建一个父工程并且在父工程中指定SpringCloud版本并且将packaing修改为pom

<packaging>pom</packaging>

<properties>
   <java.version>1.8</java.version>
   <spring.cloud-version>Hoxton.SR8</spring.cloud-version>
</properties>
<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>

2、创建eureka的server创建springboot工程并且导入依赖再启动类中添@EnableEurekaServer注解和编写yml文件

2.1、导入依赖

<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-server</artifactId>
   </dependency>
</dependencies>

2.2、启动类添加注解

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

2.3 编写yml配置文件

server:
  port: 8761

eureka:
  instance:
    hostname: localhost
  client:
    #当前的eureka是单机版的
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

2.2.2 创建EurekaClient

1、创建Maven工程修改为SpringBoot

2、导入依赖

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

3、在启动类上添加注解@EnableEurekaClient

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

4、编写配置文件

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

#指定服务名称
spring:
  application:
    name: CUSTOMER

image-20201111095518705

2.2.3 测试Eureka

1、创建了一个Search搜索模块并且注册到Eureka

2、使用EurekaClient的对象获取服务信息

@Autowired
    private EurekaClient eurekaClient;

3、创建RestTemplate

@Configuration
public class RestTemplateConfig {

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

4、使用RestTemplate调用

@GetMapping("/customer")
public String customer() {
    //1. 通过eurekaClient 获取到SEARCH服务的信息
    InstanceInfo info = eurekaClient.getNextServerFromEureka("SEARCH", false);
    //2. 获取到访问的地址
    String url = info.getHomePageUrl();
    System.out.println(url);
    //3. 通过restTemplate访问
    String result = restTemplate.getForObject(url + "/search", String.class);
    //4. 返回
    return result;
}

2.3 Eureka的安全性

实现Eureka认证

1、导入依赖

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

2、编写配置类

@EnableWebSecurity
class WebSecurityConfig extends WebSecurityConfigurerAdapter {

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

3、编写配置文件,配置用户密码

#指定用户名密码
spring:
  security:
    user:
      name: root
      password: root

4、其他服务想注册到Eureka上需要添加用户名密码

#指定Eureka服务地址
eureka:
  client:
    service-url:
      defaultZone: http://用户名:密码@localhost:8761/eureka

2.4 Eureka的高可用性

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

1、如果调用方访问过一次被调用方Eureka的宕机就不会影响到功能

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

搭建Eureka高可用

image-20201111110217411

1、准备多态Eureka

采用了复制的方式删除iml和target文件并且修改pom.xml中的项目名称再给负公差添加module

2、让服务注册到多台Eureka上

server:
  port: 8761

eureka:
  client:
    serviceUrl:
      defaultZone: http://root:root@localhost:8762/eureka/
server:
  port: 8762

eureka:
  client:
    serviceUrl:
      defaultZone: http://root:root@localhost:8761/eureka/

3、让多台Eureka之间相互通讯

eureka:
  client:
    #当前的eureka是单机版的 false单机版  true集群
    registerWithEureka: true
    fetchRegistry: true
    serviceUrl:
      defaultZone: http://root:root@localhost:8761/eureka/

2.5 Eureka的细节

1、EurekaClient启动时讲自己的信息注册到EurekaServer上EurekaServer就会储存EurekaClient的注册信息。

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

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

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

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

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

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

1、不会从EurekaServer中去移除长时间没有收到心跳的服务

2、EurekaServer还是可以正常提供服务的

3、网络稳定时EurekaServer才会开始将自己的信息被其他节点同步过去

image-20201111132523595

eureka:
  # Eureka保护机制配置
  server:
  	#true 开启  false关闭
    enable-self-preservation: true  

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

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

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

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

3.1 引言

Robbin是帮助我们实现服务和服务负载均衡

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

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

image-20201111134022333

3.2 Robbin的快速入门

1、启动两个Search模块

2、在customer导入robbin依赖

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

3、配置整合RestTemplate和Robbin

@Configuration
public class RestTemplateConfig {

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

4、在customer中去访问Search

@GetMapping("/customer")
public String customer() {
    String result = restTemplate.getForObject("http://SEARCH/search", String.class);
    //4. 返回
    return result;
}

3.3 Robbin配置负载均衡策略

1、负载均衡策略

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

2、采用注解的形式

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

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

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

四、服务间的调用-Feign

4.1 引言

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

4.2 Feign的快速入门

1、导入依赖

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

2、添加一个注解@EnableFeignClients

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

3、创建一个接口并且和search模块做映射

//指定服务名称
@FeignClient("SEARCH")
public interface SearchClient {

    //value -> 目标服务的请求路径method -> 映射请求方式
    @RequestMapping(value = "/search",method = RequestMethod.GET)
    public String search();
}

4、测试使用

@Autowired
private SearchClient searchClient;

@GetMapping("/customer")
public String customer() {    
    String result = searchClient.search();
    return result;
}

4.3 Feign的传递参数方式

1、注意事项

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

2、在Search模块下准备三个接口

@GetMapping("/search/{id}")
public Customer findById(@PathVariable Integer id) {
    return new Customer(1, "zhangsan", 23);
}

@GetMapping("/getCustomer")
public Customer getcustomer(@RequestParam Integer id, @RequestParam String name) {
    return new Customer(id, name, 23);
}

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

3、封装Customer模块的Controller

@GetMapping("/customer/{id}")
public Customer findById(@PathVariable Integer id) {
    return searchClient.findById(id);
}

@GetMapping("/getCustomer")
public Customer getcustomer(@RequestParam Integer id, @RequestParam String name) {
    return searchClient.getcustomer(id,name);
}

@GetMapping("save")
public Customer save(Customer customer) {
    return searchClient.save(customer);
}

4、再封装Client接口

@RequestMapping(value = "/search/{id}",method = RequestMethod.GET)
Customer findById(@PathVariable(value = "id") Integer id);

@RequestMapping(value = "/getCustomer",method = RequestMethod.GET)
Customer getcustomer(@RequestParam(value = "id") Integer id, @RequestParam(value = "name") String name);

@RequestMapping(value = "save",method = RequestMethod.GET) //会自动转化成POST请求  405
Customer save(@RequestBody Customer customer);

5、测试

image-20201111155409030

4.4 Feign的Fallback

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

1、创建一个POJO类实现Client接口

@Component
public class SearchClientFallBack implements SearchClient {
    @Override
    public String search() {
        return "出现问题了";
    }

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

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

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

2、修改Client接口中的注解添加一个属性

@FeignClient(value = "SEARCH",fallback = SearchClientFallBack.class)

3、添加一个配置文件。

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

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

1、FallBackFactory基于fallback

2、创建一个POJO类实现FallBackFactory

@Component
public class SearchClientFallBackFactory implements FallbackFactory<SearchClient> {

    @Autowired
    private SearchClientFallBack searchClientFallBack;
    @Override
    public SearchClient create(Throwable throwable) {
        throwable.printStackTrace();
        return searchClientFallBack;
    }
}

3、修改Client接口中的属性

@FeignClient(value = "SEARCH",
        fallbackFactory = SearchClientFallBackFactory.class
)

五、服务的隔离及熔断器-Hystrix

5.1 引言

image-20201111162630381

5.2 降级机制实现

1、导入依赖

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

2、添加一个注解@EnableCircuitBreaker

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

3、针对某一个接口去编写他的降级方法

@GetMapping("/customer/{id}")
@HystrixCommand(fallbackMethod ="findByIdFallBack" )
public Customer findById(@PathVariable Integer id) {
    int i = 1/0;
    return searchClient.findById(id);
}

//findById的降级方法方法描述要与接口一致
public Customer findByIdFallBack(Integer id) {
    return new Customer(-1,"",0);
}

4、在接口上添加注解

@HystrixCommand(fallbackMethod ="findByIdFallBack" )

5、测试

image-20201111164239591

5.3 线程隔离

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

1、Hystrix线程池默认接收用户请求采用tomcat的线程池执行业务代码调用其他服务时采用Hystrix的线程池。

2、信号量使用的还是Tomcat的线程池帮助我们取关了Tomcat的线程池

1、Hystrix的线程池的配置(具体的配置属性需要去查看HystrixCommandProperties类) wiki

1. 线程隔离策略name = hystrix.command.default.execution.isolation.strategy,value=THREAD,SEMAPHORE 2. 指定超时时间针对线程池name= hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds ,value =1000 3. 是否开启超时时间配置name=hystrix.command.default.execution.timeout.enabled,value=true 4. 超时之后是否中断线程name=hystrix.command.default.execution.isolation.thread.interruptOnTimeout,value=true 5. 取消任务之后是否中断线程name=hystrix.command.default.execution.isolation.thread.interruptOnCancel,value=false

2、信号量配置信息

  1. 线程隔离策略name = hystrix.command.default.execution.isolation.strategy,value=THREAD,SEMAPHORE
  2. 指定信号量的最大并发请求数name=hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests,value=10

5.4 断路器

5.4.1 断路器介绍

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

image-20201111181650929

5.4.2 配置断路器的监控界面

1、导入依赖

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

2、在启动类中添加注解@EnableHystrixDashboard

@EnableEurekaClient
@SpringBootApplication
@EnableFeignClients
@EnableCircuitBreaker
@EnableHystrixDashboard
@ServletComponentScan("cn.zyjblogs.servlet")
public class CustomerApplication {
    public static void main(String[] args) {
        SpringApplication.run(CustomerApplication.class,args);
    }
}

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

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

4、在启动类上添加扫描Servlet的注解@ServletComponentScan("cn.zyjblogs.servlet")

5、配置文件

hystrix:
  dashboard:
    proxy-stream-allow-list: localhost

6、测试

直接访问: http://host:port/hystrix.stream

1605103421256

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

1605103436874

5.4.3 配置断路器的属性

断路器的属性(10秒之内)

1. 断路器的开关name=hystrix.command.default.circuitBreaker.enabled,value=true 2. 失败阈值的总请求数name=hystrix.command.default.circuitBreaker.requestVolumeThreshold,value=20 3. 请求总数失败率达到%多少时打开断路器name=hystrix.command.default.circuitBreaker.errorThresholdPercentage,value=50 4. 断路器open状态后多少秒是拒绝请求的name=hystrix.command.default.circuitBreaker.sleepWindowInMillisecondsvalue=5000 5. 强制让服务拒绝请求name=hystrix.command.default.circuitBreaker.forceOpen,value=false 6. 强制让服务接收请求name=hystrix.command.default.circuitBreaker.forceClosed,value=false

具体配置方式

@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")
})

5.5 请求缓存

5.5.1 请求缓存的介绍

1、请求缓存的声明周期是一次请求

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

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

1605104987931

5.5.2 请求缓存的实现

1、创建一个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缓存被清空");
    }

}

2、使用请求缓存的注解@CacheResult CacheRemove

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

3、修改Search模块的结果返回值

 return new Customer(id, name, (int) (Math.random() * 100000));

4、编写Filter去构建HystrixRequestContext

@WebFilter("/*")
public class HystrixRequestContextFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HystrixRequestContext.initializeContext();
        filterChain.doFilter(servletRequest,servletResponse);
    }
}

5、修改Controller

@Autowired 
private CustomerService customerService;

 @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"),
      })
  public Customer findById(@PathVariable Integer id) throws InterruptedException {
    System.out.println(Thread.currentThread().getName());
    if (id == 1) {
      int i = 1 / 0;
    }
    //一下为添加内容
    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));
    customerService.clearFindById(id);
      
    return searchClient.findById(id);
  }

6、测试

1605106759891

六、服务的网关-Zuul

6.1 引言

1、客户端维护大量的ip和port信息直接访问指定服务

2、认证和授权操作需要在每一个模块中添加认证和授权操作

3、项目迭代服务拆分服务要合并需要客户端镜像大量的变化

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

网关

6.2 Zuul的快速入门

1、创建Maven项目修改SpringBoot

2、导入依赖

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

3、添加注解@EnableZuulProxy @EnableEurekaClient

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

4、编写配置文件

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

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

5、测试

image-20201112113609523

6.3 Zuul常用配置信息

6.3.1 Zuul的监控界面

1、导入依赖

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

2、编写配置文件

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

3、直接访问:http://localhost/actuator/routes

image-20201112132217601

6.3.2 忽略服务设置

# zuul的配置
zuul:
  #基于服务名忽略服务,无法查看,如果要忽略全部的服务,"*",默认配置的全部路径都会被忽略掉(自定义服务配置,通过这种方式是无法忽略的)
  ignored-services: eureka
  #监考界面依然可以查看在访问的时候404无法访问
  ignored-patterns: /**/search/**

6.3.3 自定义服务配置

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

6.3.4 灰度发布

1、添加一个配置类

@Configuration
public class ZuulConfig {
    @Bean
    public PatternServiceRouteMapper serviceRouteMapper() {
        return new PatternServiceRouteMapper(
                "(?<name>^.+)-(?<version>v.+$)",
                "${version}/${name}");
        //服务名-v版本
        //    /v版本/路径
    }
}

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

version: v1

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

3、修改Zuul的配置

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

4、修改CustomerController

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

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

5、测试

image-20201112135410803

image-20201112135429409

6.4 Zuul的过滤器执行流程

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

image-20201112140312697

6.5 Zuul过滤器入门

1、创建POJO类继承ZuulFilter

@Component
public class ZuulFilterTest extends ZuulFilter {
   
}

2、指定当前过滤器的类型

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

3、指定过滤器的执行顺序

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

4、配置是否启用

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

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

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

6、测试

image-20201112141523371

6.6 PreFilter实现token校验

1、准备访问路径请求参数专递token

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

2、创建AuthenticationFilter

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

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

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

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

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

@Override
    public Object run() throws ZuulException {
        System.out.println("AuthenticationFilter执行了");
        //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)) {
            System.out.println("token校验失败");
            //4. token校验失败直接响应数据
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
        }
        return null;
    }

4、测试

image-20201112145449105

6.7 Zuul的降级

1、创建POJO类实现接口FallbackProvider

@Component
public class ZuulFallBack implements FallbackProvider {
    
}

2、重写两个方法

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

@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
    System.out.println("降级的服务"+route);
    cause.printStackTrace();
    return new ClientHttpResponse() {
        @Override
        public HttpHeaders getHeaders() {
            //指定响应头信息
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            return headers;
        }

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

        @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() {

        }
    };
}

3、测试

image-20201112151632111

6.8 Zuul动态路由

1、创建一个过滤器

@Component
public class DynamicRoutingFilter  extends ZuulFilter{
    
}

2、在run方法中编辑业务逻辑

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

@Override
public int filterOrder() {
    return FilterConstants.PRE_DECORATION_FILTER_ORDER +3;
}

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

@Override
public Object run() throws ZuulException {
    System.out.println("DynamicRoutingFilter执行了");
    //1、获取Request对象
    RequestContext context = RequestContext.getCurrentContext();
    HttpServletRequest request = context.getRequest();

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

    //3、直接判断
    if (redisKey != null && redisKey.equalsIgnoreCase("customer")) {
        System.out.println("DynamicRoutingFilter执行了路由到了/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")) {
        System.out.println("DynamicRoutingFilter执行了路由到了/search/1");
        //http://localhost:8081/search
        context.put(FilterConstants.SERVICE_ID_KEY,"search");
        context.put(FilterConstants.REQUEST_URI_KEY,"/search/1");
    }
    return null;
}

3、测试

image-20201112155707148

image-20201112155652269

七、多语言支持-Sidecar

7.1 引言

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

image-20201112160258956

7.2 Sidecar实现

1、创建一个第三方的服务

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

2、创建Maven工程修改为SpringBoot

2、导入以来

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

4、添加注解@EnableSidecar

5、编写配置文件

server:
  port: 81

eureka:
  client:
    service-url:
      defaultZone:  http://root:root@localhost:8761/eureka,http://root:root@localhost:8762/eureka

#指定服务名称
spring:
  application:
    name: OTHER-SERVICE    #other-service

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

6、通过customer通过Feign的方式调用第三方服务

image-20201112165325023

八、服务间消息传递-Stream

8.1 引言

用于构建消息驱动微服务的框架(在下面方便起见也叫它Stream框架)该框架在Spring Boot的基础上整合了Spring Integration来连接消息代理中间件RabbitMQKafka等。它支持多个消息中间件的自定义配置同时吸收了这些消息中间件的部分概念例如持久化订阅、消费者分组和分区等概念。使用Stream框架我们不必关系如何连接各个消息代理中间件也不必关系消息的发送与接收只需要进行简单的配置就可以实现这些功能了可以让我们更敏捷的进行开发主体业务逻辑了。

Spring Cloud Stream框架的组成部分

  1. Stream框架自己的应用模型
  2. 绑定器抽象层可以与消息代理中间件进行绑定通过绑定器的API可实现插件式的绑定器。
  3. 持久化订阅的支持。
  4. 消费者组的支持。
  5. Topic分区的支持。

image-20201112165641574

8.2 Stream快速入门

1、启动RabbitMQ

2、消费者-导入依赖

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

3、消费者-配置文件

spring:
  rabbitmq:
    port: 5672
    username: test
    password: test
    virtual-host: /test

4、消费者-监听的队列

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);
    }
}

5、启动类添加注解@EnableBinding(StreamClient.class)

6、生产者-导入依赖

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

7、生产者-配置文件

spring:
  rabbitmq:
    port: 5672
    username: test
    password: test
    virtual-host: /test

8、生产者-发布消息

public interface StreamClient {
    @Output("myMessage")
    MessageChannel output();
}
@RestController
public class MessageController {
    @Autowired
    private StreamClient streamClient;
    @GetMapping("/send")
    public String send() {
        streamClient.output().send(MessageBuilder.withPayload("Hello Stream").build());
        return "消息发送成功!!";
    }
}

9、启动类添加注解@EnableBinding(StreamClient.class)

10、测试访问http://localhost:8080/send

image-20201113093057704

image-20201112173342142

8.3 Stream重复消费问题

只需要添加一个配置,指定消费者组

spring:
  cloud:
    stream:
      binders:
        myMessage:  #队列名称
          group: customer #消费者组

8.4 Stream的消费者手动ack

1、编写配置

#指定服务名称
spring:
  cloud:
    stream:
      #实现手动ack
      rabbit:
        bindings:
          myMessage:
            consumer:
              acknowledgeMode: MANUAL

2、修改消费端方法

@Component
@EnableBinding(StreamClient.class)
public class StreamReceiver {
    @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 引言

1、配置文件分散在不同项目中的不方便去维护。

2、配置文件的安全问题。

3、修改配置文件无法立即生效。

image-20201112180552221

9.2 搭建Config-Server

1、创建Maven工程修改SpringBoot

2、导入依赖

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

3、添加注解@EnableConfigServer

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

4、编写配置文件git

spring:
  cloud:
    config:
      server:
        git:
          basedir:     E:\config # 本地仓库地址
          username:    xxxxxx@xxxx.com #远程仓库的用户名
          password:    xxxxxxxx #远程仓库的密码
          uri: https://gitee.com/zyjblog/config-resp.git

5、测试(例子:http://localhost:82/master/customer-xxx.yml (master可以省略)

访问方式如下:

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

/{application}-{profile}.properties /{label}/{application}-{profile}.properties

image-20201113094055276

9.3、修改Customer连接Config

1、导入依赖

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

2、修改配置文件

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

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

3、修改配置文件名称application.yml改为bootstrap.yml

4、测试测试发布消息到RabbMQ

image-20201113102316294

9.4 实现动态配置

9.4.1 实现原理

image-20201113103152115

9.4.2 服务连接RabbitMQ

1、导入依赖

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

2、编写配置文件

spring:
  rabbitmq:
    virtual-host: /test
    host: localhost
    username: test
    password: test
    port: 5672

3、测试

image-20201113104134210

9.4.3 实现手动刷新

1、导入依赖(两个服务config和customer均添加)

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

2、编写配置文件(两个服务config和customer均添加)

management:
  endpoints:
    web:
      exposure:
        include: "*"

3、为customer添加一个controller,添加注解@RefreshScope

@RestController
@RefreshScope
public class CustomerController {

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

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

4、测试

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

9.4.4 内网穿透

1、内网穿透官网http://www.ngrok.cc/

2、注册登录

3、购买免费隧道并配置

image-20201113113814310

4、下载客户端并复制隧道id点击运行客户端复制到客户端中

image-20201113114050201

5、测试访问是否成功

image-20201113114131931

9.4.5 实现自动刷新配置

1、配置Gitee中的WebHooks

image-20201113120733998

2、给Config添加一个过滤器UrlFilter


@WebFilter("/*")
public  class UrlFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest= (HttpServletRequest) servletRequest;
        String url=httpServletRequest.getRequestURI();
        System.out.println(url);
        if(!url.endsWith("/actuator/bus-refresh")){
            filterChain.doFilter(servletRequest,servletResponse);
            return;
        }
        String body=(httpServletRequest).toString();
        System.out.println("original body: "+ body);
        RequestWrapper requestWrapper=new RequestWrapper(httpServletRequest);
        filterChain.doFilter(requestWrapper,servletResponse);
    }
    private class RequestWrapper extends HttpServletRequestWrapper {
        public RequestWrapper(HttpServletRequest request) {
            super(request);
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            byte[] bytes = new byte[0];
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
            ServletInputStream servletInputStream = new ServletInputStream() {
                @Override
                public int read() throws IOException {
                    return byteArrayInputStream.read();
                }

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

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

                @Override
                public void setReadListener(ReadListener listener) {

                }
            };
            return servletInputStream;
        }
    }
}

3、添加注解@ServletComponentScan("cn.zyjblogs.filter")

@SpringBootApplication
@EnableConfigServer
@ServletComponentScan("cn.zyjblogs.filter")
public class ConfigApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigApplication.class,args);
    }
}

3、测试

image-20201113121035781

十、服务的追踪-Sleuth

10.1 引言

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

1、Sleuth可以获取得到整个服务链路的信息

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

3、Sleuth将日志信息存储到数据库中

image-20201113131325996

10.2 Sleuth使用

1、导入依赖

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

2、编写配置文件

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

3、测试

SEARCH服务名称

012 总链路id

b0e当前服务的链路id

false不会将当前的日志信息输出到其他系统中

image-20201113133658258

image-20201113133713872

image-20201113133745985

10.3 Zipkin的使用

1、搭建Zipkin的web工程 https://zipkin.io/pages/quickstart

  1. docker安装Zipkin

1、使用docker pull拉取

docker pull openzipkin/zipkin

2、使用docker-compose

version: "3.1"
services:
  zipkin:
    image: daocloud.io/daocloud/zipkin:latest
    restart: always
    container_name: zipkin
    ports:
    - 9411:9411
docker-compose up -d  #启动
docker-compose down   #关闭

2、导入依赖

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

3、编写配置文件

spring:
  sleuth:
    sampler:
      probability: 1 #百分之多少的sleuth信息需要输出到zipkin
  zipkin:
    base-url: http://127.0.0.1:9411/  #指定zipkin的地址

4、测试

image-20201113153443738

10.4 整合RabbitMQ

1、导入RabbitMQ依赖zipkin中已经依赖了RabbitMQ了

2、修改配置文件

spring:
  zipkin:
    sender:
      type: rabbit

3、修改Zipkin信息

version: "3.1"
services:
  zipkin:
    image: daocloud.io/daocloud/zipkin:latest
    #image: docker.io/openzipkin/zipkin:latest
    restart: always
    container_name: zipkin
    ports:
    - 9411:9411
    environment:
      - RABBIT_ADDRESSES=10.27.10.123:5672 #本地ipv4地址:端口
      - RABBIT_USER=test
      - RABBIT_PASSWORD=test
      - RABBIT_VIRTUAL_HOST=/test

3、测试

image-20201113160658535

image-20201113160724897

10.5 Zipkin存储数据到ES

1、重新修改zipkin的文件yml文件

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

2、安装Es

 #拉取镜像
 docker pull elasticsearch
 #启动参数
 docker run --name es1_6.6.0 \
-p 9200:9200 \
-p 9300:9300 \
-e ES_JAVA_OPTS="-Xms256m -Xmx256m" \ 
-v /d/elasticsearch/config/es1.yml:/usr/share/elasticsearch/config/elasticsearch.yml \ 
-v /d/elasticsearch/data/es1:/usr/share/elasticsearch/data \ 
-v /d/elasticsearch/logs/es1:/usr/share/elasticsearch/logs \
-d 13aa43015aa1

3、安装kibana

#拉取kibana
docker pull kibana
#启动参数
docker run --name kibana6.6.0 -e ELASTICSEARCH_URL=http://10.27.10.123:9200 -p 5601:5601 -d dfc685453eaa

创建索引

image-20201113164636525

4、重启zipkin后数据未丢失

docker-compose restart

5、测试

image-20201113164930192

十一、完整SpringCloud架构图

image-20201113165759501