死亡三连问
SpringCloud 是啥?微服务 是啥?为啥 需要微服务? 一些概念性的东西
SpringCloud的主要应用过程 开始操作
完整的文件结构搭建基础框架Eureka注册中心Ribbon负载均衡Hystrix 熔断器Feign远程调用
Feign的熔断器 GateWay网关
路由前缀过滤器 死亡三连问 SpringCloud 是啥?
官方一点的说法:SpringCloud是在SpringBoot基础上构建的,用于快速构建分布式系统的通用模式的工具集(注意了:重点在于它只是一个工具),因此我把它简单的理解为构建微服务的一个工具SpringCloud主要就是将各家较为成熟的服务框架组合起来,再通过SpringBoot风格进行封装,屏蔽掉了原先复杂的配置和实现原理,最终让开发者可以简单方便且易维护的开发的分布式系统开发工具包那么SpringCloud和云到底有啥关系呢,其实由于使用SpringCloud开发的应用程序非常适合在Docker或Pass上部署,所以由SpringCloud构建的程序又叫做云原生应用,可以简单的理解为面向云环境的软件架构 微服务 是啥?
微服务,见名知义,就是较小的服务单元,与微服务相对的单体架构单体架构 单体架构就是将所有的业务场景(表现层、业务逻辑层、数据访问层)都放在一个工程中,部署在一台服务器上那么相对单体架构的微服务架构就是将单一程序开发成一个微服务每个微服务运行在自己的进程中,并使用轻量级的机制通信,通常是HTTP RESTFUL API(也可以采用消息队列来通信)。这些服务围绕着业务能力来划分,并通过自动化部署机制来独立部署(如Jenkins),降低出错频率这些服务可以使用不同的编程语言,不同的数据库,以保证最低限度的集中式管理(例如Eureka,Zookeeper)。 为啥 需要微服务?
一般呢,小的公司是用不到微服务的,怎么样?是不是这个技术瞬间高大上了起来微服务架构的出现 当然是由于单体架构不给力导致的在业务的逻辑越来越复杂的时候,单体架构的代码可读性和可维护性就会越来越低,面对海量用户时,单体架构系统的并发能力也会随之下降那么就需要使用微服务架构将一个复杂业务拆分成多个小业务,将复杂问题不断拆分成一个个简单的小问题微服务是分布式系统,业务与业务之间完全解耦,也就是说一个业务出现了问题,不会影响其他的业务,可以显著提高生产力同时当我们需要增加业务时,可以根据业务再拆分,具有极强的横向扩展能力,面对高并发的场景可以将服务集群化部署,加强系统负载能力因为现实中,一个工程可能由多个服务单元组成,但是多个服务可能使用不同的技术或语言实现,微服务可以让这种融合更自由 一些概念性的东西 SpringCloud的主要应用过程
首先展示一下源自网络的springCloud组件架构图:
结合这张图来说一下springCloud的应用过程:
所有的请求(移动端、客户端有一个算一个)都统一通过网关服务(Zuul Proxy)来访问内部服务网关接收到请求后,从注册中心(Eureka)获取可用的微服务,所有的微服务都需要在注册中心进行注册其中Ribbon主要用于负载均衡,它可以帮助判断并分发具体的请求到后端的具体应用服务实例,起到了分发压力的作用微服务(services)之间通过Feign进行通信处理业务,保持服务的一致性Hystrix负责处理服务超时熔断Turbine监控 服务间的调用和熔断的相关指标
从上面这张图中也可以看出SpringCloud主要的组件,解释一下这些组件都具体是做什么用的:
Netflix Eureka 该组件负责服务的注册与发现,Eureka体系包括:服务注册中心、服务提供者、服务消费者Netflix Hystrix 熔断器,容错管理工具,旨在通过熔断机制控制服务和第三方库的节点。比如突然某个服务出现了故障,当请求多时,就会发生严重的阻塞影响整体服务响应。Hystrix会及时发现某个不在状态不稳定服务,将其他服务调过来响应Netflix Zuul 该组件是在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。Zuul相当于设备和Netflix流应用的Web网站后端所有请求的前门Netflix Archaius 该组件用于配置管理API,包含一系列配置管理API,提供动态类型化属性、线程安全配置操作、轮询框架、回调机制等功能。原理是每隔60s从配置源读取一次内容,这样修改配置文件后不需要重启服务就可以使修改后的内容生效,前提是使用archaius的API来读取Spring Cloud Config 配置中心,配置管理工具包,可以将配置放在远程服务器,集中化管理集群配置,目前支持本地存储、GIt以及SubVersion。方便以后统一管理、升级装备Spring Cloud Bus 事件、消息总线,用于在集群中传播状态变化,可以与Spring Cloud Config联合实现热部署Spring Cloud for Cloud Foundry 一个开源Paas云平台,支持多种框架、语言、运行时环境、云平台以及应用服务,使开发人员可以在几秒钟内进行应用程序的部署和扩展,无需担心任何基础架构问题Spring Cloud Cluster 提供在分布式系统中的集群所需要的基础功能支持,如选举、集群的状态一致性、全局锁、tokens等常见状态模式的抽象和实现Spring Cloud Consul Consul是一个支持多数据中心分布式高可用的服务发现和配置共享的服务软件,consul是一个服务发现与配置工具,与Docker容器可以无缝集成 开始操作 完整的文件结构
整个项目分为四个模块consumer、provider、euraka、gateway
consumer结构
eureka结构
gateway文件结构
provider文件结构
IDEA创建maven项目
更改父级Pom文件
完整的父级Pom文件如下:
<?xml version="1.0" encoding="UTF-8"?>
主要的部分就是Properties和dependency两部分,本次测试项目中并没有使用到数据库,但实际业务需要使用到,所以记得添加
创建生产者Provider
在项目的父级目录上右键new->module->maven
更改provider的pom文件
<?xml version="1.0" encoding="UTF-8"?>
添加provider的基础文件
Controller层代码如下
@RestController@RequestMapping("provider")public class ProviderController { //使用Map模拟数据库 private Map
pojo层代码如下
@Data@NoArgsConstructor@AllArgsConstructorpublic class User { private Integer id; private String name;}
ProviderApplication启动类代码如下
@SpringBootApplicationpublic class ProviderApplication { public static void main(String[] args) { SpringApplication.run(ProviderApplication.class); }}
Provider启动测试
启动provider的启动类,使用postman测试
如下即provider基础编写完成
添加消费者Consumer模块
步骤同Provider
更改consumer的pom文件
<?xml version="1.0" encoding="UTF-8"?>
添加Consumer基础代码
Controller层代码如下:
@RestController@RequestMapping("consumer")public class ConsumerController { @Autowired private RestTemplate restTemplate; @GetMapping("/findAll") public List
pojo层代码如下
和provider的完全一致
Consumer启动类代码如下
@SpringBootApplicationpublic class ConsumerApplication { //springBoot没有主动注入RestTemplate,需要我们手动注入 @Autowired private RestTemplateBuilder templateBuilder; @Bean public RestTemplate restTemplate() { return templateBuilder.build(); } public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class); }}
Consumer代码启动测试
因为同时Provider代码也必须是运行状态,所以需要先给provider改一下运行端口号
provider的application.yml文件中添加
server.port: 8081
这里有个idea小技巧,为了看到所有的模块运行情况,我们需要底栏显示Services
找到本项目的.idea文件夹下面的workspace.xml文件,在文件末尾添加
后重启IDEA使之生效
启动consumer,provider
创建一个子模块eureka
方式同consumer、provider
更改pom文件
<?xml version="1.0" encoding="UTF-8"?>
因为Eureka只是注册中心,没有实际业务,所以完整的文件结构如下
启动类EurekaApplication中代码
@SpringBootApplication@EnableEurekaServer //声明当前模块是一个Eureka服务端public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class); }}
配置文件
server: # 实际业务不要用这个端口号,易被攻击 port: 8761#声明注册中心的urleureka: client: service-url: defaultZone: http://0.0.0.0:8761/eureka # 关闭注册自己 fetch-registry: false register-with-eureka: false
Done!! 启动程序,url访问localhost:8761
我们看到Instance currently registered with Eureka中空空如也,这是因为我们没有进行对微服务的注册,重申一遍:所有的微服务都需要注册到注册中心
将provider注册到注册中心
1、在Provider的pom文件中增加eureka client的依赖
2、启动类上加注解
@EnableDiscoveryClient这里想说一下@EnableDiscoveryClient和@EnableEurekaClient从表面意思上看,似乎是@EnableEurekaClient更应该用在这里,但实际上这个注解已经是老版了,应用场景单一
并且使用后会在界面显示这样的信息
3、配置文件增加配置
server: port: 8081# 服务名称spring: application: name: provider#eureka配置eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka
启动后显示eureka页面显示已经注册了一个实例
将consumer注册到注册中心
步骤同provider,相同部分不再赘述
consumer中通过注册中心的方式调用provider
ConsumerController改为如下内容
@RestController@RequestMapping("consumer")public class ConsumerController { @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; @GetMapping("/findAll") public List
运行Consumer后使用postman调用findAll接口,控制台显示如下
首先我们需要启动两个provider
只需在启动了一个provider后,更改端口号再启动一个provider即可
启动完成后,在注册中心会看到有两个provider
在Consumer的RestTemplate上面添加@LoadBalanced
该注释是开启负载均衡的作用
@SpringBootApplication@EnableDiscoveryClientpublic class ConsumerApplication { //springBoot没有主动注入RestTemplate,需要我们手动注入 @Autowired private RestTemplateBuilder templateBuilder; @Bean @LoadBalanced //开启负载均衡 只增加了这里请注意,其余都不变 public RestTemplate restTemplate() { return templateBuilder.build(); } public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class); }}
在Consumer的Controller层更改访问provider的形式
开启负载均衡之后,可以直接使用实例名称进行访问接口,而无需通过discoveryClient自己手动获取
以下仅显示Conusmer的findAll的更改,其余模块没有更改
@GetMapping("/findAll") public List
启动所有微服务后,访问consumer的接口
可以看到是拿到了结果,那么我们怎么知道到底是访问了那个Provider呢,虽然实际是不用关心的,但在学习阶段我们最好关心以下
通过打印日志可以看到,刚刚请求的 8082端口的provider
PS:我们还可以通过配置文件更改负载均衡的策略
比如当前默认的是轮询机制,我们可以配置成随机之类的
研究了一下,好像是如果要针对某个服务配置负载均衡策略,就需要在配置文件里增加
#RandomRule 随机#RoundRobinRule 轮询#provider是服务名,记得切换称自己的provider: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
如果设置全局负载均衡策略,就需要在consumer的配置类中返回一个配置
像这样
@Bean public IRule defaultLBStrategy(){ return new RandomRule(); }
!!!需要注意的是,两者不能同时用,用其中一个的时候,记得注释另一个
Hystrix 熔断器 插播! 插播! 紧急插播 Hystrix原理
先上手操作,再了解一下原理,还是蛮重要的
Consumer的Pom文件中增加Hystrix的依赖
Conusmer的启动类添加注释开启熔断
因为我们这里只测试一个Provider的熔断,所以记得取消负载均衡的注释
Consumer的配置文件中设置熔断器的配置
# 配置熔断策略:hystrix: command: default: circuitBreaker: # 原理分析中解释配置含义 # 强制打开熔断器 默认false关闭的。测试配置是否生效 forceOpen: false # 触发熔断错误比例阈值,默认值50% errorThresholdPercentage: 50 # 熔断后休眠时长,默认值5秒 sleepWindowInMilliseconds: 5000 # 熔断触发最小请求次数,默认值是20 requestVolumeThreshold: 10 execution: isolation: thread: # 熔断超时设置,默认为1秒 timeoutInMilliseconds: 2000
在Conusmer的Controller层编写降级的FallBack方法
@RestController@RequestMapping("consumer")public class ConsumerController { @Autowired private RestTemplate restTemplate; @Autowired private DiscoveryClient discoveryClient; @HystrixCommand(fallbackMethod = "findAllFallBack") @GetMapping("/findAll") public List
!! 注意:FallBack方法的返回值必须和FindAll方法一致
在Provider的findAll方法中让线程睡几秒,触发熔断
因为我们配置的触发熔断时间为1秒,所以当请求返回超过1秒时就会触发熔断
熔断效果
在请求时,没有得到正确返回,触发了熔断
Cosumer的运行控制台输出了FallBack方法中的内容
PS:当我们需要所有的请求都共用一个FallBack方法时,可以配置全局熔断器
只需要两步
1、在Controller类(注意这里是类不是方法)上添加注解 @DefaultProperties(defaultFallback = “defaultFallback”)
2、在目标方法上添加注解 @HystrixCommand (注意不需要再次指定fallbackMethod)
ps:暗戳戳的说一句,感觉也没有省事到哪里去
老样子,先操作后理解
插播! 插播! Feign的插播
Consumer的pom文件中添加依赖
Consumer启动类中添加开启Feign客户端的注释
@EnableFeignClients //开启feign客户端
Consumer中编写Feign客户端
feign客户端在Consumer里就是一个仿照ProviderController中方法的接口,没有实质的方法实现
@FeignClient(value = "provider",path = "/provider")public interface ConsumerClient { //伪装成ProviderController中的findAll @GetMapping("/findAll") List
Consumer中的Controller使用Feign发送请求
@RestController@RequestMapping("consumer")public class ConsumerController { //注入Feign客户端 @Qualifier("com.dean.feign.ConsumerClient") @Autowired private ConsumerClient consumerClient; @GetMapping("/findAll") public List
启动后发送请求可正常获得结果
Feign的熔断器
是的,Feign里面也结合了熔断器,让熔断器的实现更加容易
如果需要进一步配置feign的熔断器,需要添加依赖
然后在配置文件里进行配置
feign: hystrix: enabled: truehystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 15000 threadpool: default: coreSize: 40 maximumSize: 100 maxQueueSize: 100
如不需要这些高级配置只需要按下列步骤进行即可
Consumer里实现ConsumerClient
我们之前写了一个接口ConsumerClient假装是ProviderController,那么当我们实现它的时候,它就成为了一个编写降级服务的类
@Componentpublic class ConsumerFallBack implements ConsumerClient { //feign Hystrix 服务降级方法 @Override public List
Consumer的配置文件中编写Feign Hystrix的配置
因为Feign内置了熔断器,我们就不再需要熔断器本身的一些配置和注解了,只需要设置Feign的Hystrix配置即可
# feign 熔断器feign: hystrix: enabled: true
Consumer的Feign客户端添加FallBack所在的类
@FeignClient(value = "provider",path = "/provider",fallback = ConsumerFallBack.class)public interface ConsumerClient { //伪装成ProviderController中的findAll @GetMapping("/findAll") List
启动后,可以看到熔断器有效
Consumer的控制台这边可以看到输出了我们降级方法中的内容
GateWay网关
创建一个SpringBoot组件Gateway
方式同创建provider 和consumer
文件结构如下:
Gateway的pom文件中添加依赖
注意这里有个大坑,因为我们之前在父工程的中添加了spring-boot-starter-web的依赖,但是网关使用时不能有这个依赖,所以需要我们从父工程中移除,分别添加到使用到这个依赖的子工程中(在本项目里就是consumer、provider、eureka)
一定要移除,移除完成后在gateway的pom文件中添加如下依赖
编写Gateway启动类GateWayApplication
注意网关也只是个微服务,需要注册到注册中心
@EnableDiscoveryClient@SpringBootApplicationpublic class GateWayApplication { public static void main(String[] args) { SpringApplication.run(GateWayApplication.class); }}
配置Gateway的配置文件Appliaction.yml
网关重在配置
server: port: 9000# 服务名称spring: application: name: gateway # 网关配置 cloud: gateway: # 全局配置 globalcors: cors-configurations: '[/**]': # 匹配所有请求 allowedOrigins: "*" #跨域处理 *表示通配 允许所有的域 实际可以填写ip allowedMethods: # 支持的方法 - GET - POST - PUT - DELETE # 网关路由配置 routes: - id: provider-router # 路由id 唯一标识 uri: lb://provider # 路由地址,动态路由 lb是一个网关的协议 predicates: # 断言 - Path=/provider/** # 请求url 多个使用逗号 即所有的/provider/**请求会交由uri中写的服务provider来处理#eureka配置eureka: client: service-url: defaultZone: http://127.0.0.1:8761/eureka
编写过滤器TokenFilter
import org.springframework.cloud.gateway.filter.GatewayFilterChain;import org.springframework.cloud.gateway.filter.GlobalFilter;import org.springframework.core.Ordered;import org.springframework.stereotype.Component;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Mono;@Componentpublic class TokenFliter implements GlobalFilter, Ordered { @Override public Mono
完成后顺序启动Eureka、Provider、GateWay
可以看到注册中心出现了两个服务provider和gateway
本来我们访问provider的findAll方式是这样
也就是访问的是provider服务
但当我们配置了网关以后,可以将provider的真正服务端口号隐藏起来,访问网关服务来访问到findAll
网关控制台这边也可以看到我们通过TokenFliter中的fliter方法打印的字样
路由前缀
网关的配置文件中更改配置(隐藏前缀)
在gateway中可以通过配置路由的过滤器PrefixPath来实现映射路径的前缀添加,可以起到隐藏接口地址的作用,避免接口地址暴露
啥意思呢
就是以我们的provider来看,我们之前请求路径是/provider/findAll,但通过配置之后我们可以隐藏/provider,只访问findAll即可
predicates: - Path=/**filters: - PrefixPath=/provider
测试
可以看到通过该配置,即隐藏了实际服务的端口号也隐藏了请求的前缀
网关的配置文件中更改配置(增加前缀)
在gateway中通过配置路由过滤器StripPrefix,实现映射路径中的地址的去除。
通过StripPrefix=1来指定路由中需要去除的前缀个数
例如我们通过访问路径/api/provider/findAll将会被路由解析到/provider/findAll
predicates: - Path=/api/**filters: - StripPrefix=1
测试
PS:以上两个也可以都加
predicates: # 断言 - Path=/api/** # filters: - StripPrefix=1 - PrefixPath=/provider
但一定要注意StripPrefix和PrefixPath的顺序问题
写成现在这样的意思就是先去掉/api,再添加/provider,所以请求的时候可以直接用/api/findAll请求
过滤器是网关的一个重要功能,实现了请求的鉴权,前面路由前缀的功能也是使用过滤器实现的
过滤器分为全局过滤器和局部过滤器
可以同时配置全局过滤器和局部过滤器
全局过滤器(实现GlobalFilter及Ordered)
例如上面所说的TokenFilter
@Componentpublic class TokenFliter implements GlobalFilter, Ordered { @Override public Mono
自定义局部过滤器
有两种实现方式
直接实现GatewayFilter接口,重写里面的filter方法继承AbstractGatewayFilterFactory类,重写apply方法(实际上底层也是重写filter方法)
以下是第二种方式的代码示例
package com.dean.filter;import org.springframework.cloud.gateway.filter.GatewayFilter;import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;import org.springframework.http.HttpHeaders;import org.springframework.http.server.reactive.ServerHttpRequest;import org.springframework.stereotype.Component;import org.springframework.util.MultiValueMap;import java.util.Arrays;import java.util.List;import java.util.function.Consumer;//类名的格式必须为XXXGatewayFilterFactory@Componentpublic class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory { public static final String AUTH_NAME = "name"; //构造函数 public AuthGatewayFilterFactory() { super(Config.class); } //如果要获得配置的-Auth的值,必须重写这个方法,否则拿不到 @Override public List
主要有几个重点:
1、类名必须是XXXGatewayFilterFactory
2、配置文件里的配置就是这个XXX
3、shortcutFieldOrder()方法必须重写,否则拿不到配置的值
gateway的yaml中路由处进行配置
# 网关路由配置 routes: #id是个列表 有多个路由配置时就写多个id及下面的配置,以下是完整的一个 - id: provider-router # 路由id 唯一标识 uri: lb://provider # 路由地址,动态路由 lb是一个网关的协议 predicates: # 断言 - Path=/**# - Path=/api/** # 请求url 多个使用逗号 即所有的/provider/**请求会交由uri中写的服务provider来处理 # 添加filter后,会在predicates上的所有请求上添加/provider # 所以实际请求就不需要再写/provider了 filters:# - StripPrefix=1 - PrefixPath=/provider - Auth=name #过滤器的名称取得是AuthGatewayFilterFactory去掉GatewayFilterFactory的部分
其实只是多了一个我们自己增加的-Auth
通过网关访问接口后控制台的输出
PS:当我们想要配置多个路由及每个路由配置自己的过滤器时
从配置中可以看出route下的-id是个列表的格式
彩蛋:GateWay自带的过滤器
gateway自带的过滤器有几十个,常见的有
例如配置文件中配置
spring: application: name: gateway cloud: gateway: default-filters: - AddResponseHeader=ilove,web
请求后就可以看到响应头添加了ilove=web了
感兴趣的话,可以去官方文档看看
以上。