SpringCloud-微服务
单体架构
将业务的所有功能集中在一个项目中开发,打成一个包部署。它的优点是架构简单、部署成本低。
缺点是团队协作成本高(项目打包时间非常长,改bug重新编译就是一种折磨)、系统分布效率低、系统可用性差(Tomcat的资源是有限的,当一个接口的访问量短时间内过高,是会影响所有接口的访问效率的,甚至会导致整个项目宕机)
所以单体项目适合功能较少,访问量较小的项目。
微服务
微服务架构,是服务化思想指导下的一套最佳实践架构方案。服务化,就是把单体架构中的功能模块拆分成多个独立的项目。当然了,拆分也是有一定的要求的,拆分粒度一定要小(按照业务来看,拆出去的项目要符合一个完整的功能,单一职责)、团队自治(每个拆分的项目都要有团队来维护)、服务自治(分开打包、分开部署)
SpringCloud(组件)
SpringCloud作为微服务全家桶的技术栈,其包含了很多组件来解决服务拆分中带来的问题
服务治理(Nacos)
如果不同服务之间存在数据的交换,那么就需要由一个服务调用另一个服务的接口,成为服务治理。服务治理的三个角色分别是
- 提供者,提供服务接口,供其他服务调用
- 调用者,调用其服务提供的接口
- 注册中心,记录并监控微服务各实例状态,推送服务变更状态
注册中心原理
所以注册中心的作用就是为消费者提供提供者的地址,消费者可以从注册中心订阅和拉取服务信息
那么消费者如何得知服务的变更状态? 服务提供者通过心跳机制向注册中心报告自己的健康状态,当心跳异常时,注册中心就会将异常的服务剔除,并通知订阅了该服务的消费者。
如果提供者有多个实例时,消费者可以通过负载均衡算法,从多个实例中选择一个
Nacos注册中心
服务注册
Nacos是目前国内占比最多的注册中心组件,在docker配置好nacos,并且导入数据库后就可以使用,然后在pom引入nacos依赖和配置。
<!-- Nacos 服务注册发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
spring:
application:
name:service1 #服务名称
cloud:
nacos:
server-addr:127.0.0.1:8848 #nacos地址
启动服务,就可以在nacos的服务管理-服务列表看到服务的实例
服务发现
消费者需要连接Nacos以拉取和订阅服务,因此服务发现的前两步和服务的注册是一样的,后面在加上服务调用即可:
- 引入Nacos discovery依赖
- 配置nacos地址
- 服务发现
使用DiscoveryClient对象的getInstances()
方法获取实例列表(传参为服务名),然后实现负载均衡(使用Random实现,Random传参为列表size),随机从实例列表中取出一个实例,里面包含了实例的全部信息(主机名、端口号、原信息等等),然后使用RestTemplate向提供者发起网络请求,获得HTTP响应。这里就不需要再写死服务提供者的端口信息,一切由Nacos注册中心提供,我们只需要写入服务名即可。
OpenFeign
如果不使用其他组件,微服务之间的调用是相当的麻烦,调用者的代码要先获取提供者服务实例列表;然后手写负载均,挑选一个实例;接着利用RestTemplate发起http请求,调用另外一个服务;最后还要进行解析结果,代码实现是很麻烦的。
OpenFeign如何解决这个问题
OpenFeign是声明式的HTTP请求客户端,可以让我们写http请求时变得更加简单、优雅。
引入OpenFeign和负载均衡(loadbalancer
)依赖,通过在类名上添加@EnableFeignClient
注解,启用OpenFeign功能。
第三步新建接口,上面标注@FeignClient("服务名")
注解,然后通过@GetMapping或者@PostMapping注解,添加请求路径参数,最后在定义方法时传递请求参数即可实现发送方的跨服务请求发送。当然了,方法不用自己实现。调用者就可以使用一行代码,调用这个接口的方法即可。
最佳实践方式
单独编写一个服务模块(api),每个模块抽取一个api模块,需要调用接口的添加对api模块的依赖。这个就可以不用更新服务就在所有的Feign接口里修改代码,只修改api模块里的即可。
当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用,所以需要指定FeignClient所在的包(@EnableFeignClient(basePackages = "api包所在的位置")
)
网关(Spring Cloud Gateway)
网关就是网络的关口,负责请求的路由、转发、身份校验。
如果不使用网关,则会存在微服务过多,前端不知道该请求哪个服务的问题。加上了网关后,前端只需要知道网关的地址就够了,网关在根据前端的请求,再去判断应该由哪个微服务去处理这个请求。这个判断的过程就是请求的路由。
接下来网关就会把请求转发到具体的微服务,这个过程就行路由的转发,如果一个微服务有多个实例,则会在多个实例之间进行负载均衡。
当然了,在网关在路由转发之前,是肯定在请求的用户的身份进行校验,就不用在每个微服务之间做身份验证了。
网关会从注册中心拉取这些微服务的地址,如果服务地址有变更,nacos将修改后的地址推送给网关。不仅如此,有了网关之后,每个微服务的地址就不会再暴露给前端,暴露的只有网关的地址,这对于微服务来说也是一种保护。
SpringCloud中的网关的实现包括两种,Spring Cloud Gateway
和 Netfilx Zuul
Netfilx Zuul
- Netfix出品
- 基于Servlet的阻塞式编程
- 需要调优才能获得SpringCloudGateWay类似的性能
Spring Cloud Gateway
- Spring官方出品
- 基于WebFlux响应式编程
- 无需调优即可获得优异性能
响应式编程的吞吐能力要比阻塞式优秀很多,Spring Cloud Gateway 是现在主力的网关。
引入相关依赖和配置,routes
是路由,我们可以配置的路由规则不止一个,-开头,-就是代表一个元素。每个元素里有三个关键属性,id是路由的唯一标识(最好跟服务名保持一致);uri代表路由到哪个微服务;前端请求到达网关后,网关需要对请求作出判断,由哪个微服务进行处理,predicates是判断的依据。
路由属性
网关路由对应的Java类型是RouteDefinition,其中常见的属性有:
- id:路由的唯一标识
- uri:路由目标的地址
- predicates:路由断言,判断请求是否符合当前的路由
- filters:路由过滤器,对请求或相应做特殊处理
Spring提供了12种路由断言
网关中提供了30多种路由过滤器,每种过滤器都有独特的作用。
这段添加的过滤器就是为每个请求添加请求头,然后可以在Controller的方法中传参时加入@RequestHeader(value = "truth")
拿到请求头。
-id:
uri:
predicates:
filters:
-AddRequestHeader=truth, you good
网关登录校验
很多微服务在处理请求时都需要做登录校验,在所有的微服务都添加登录校验代码是不现实的,而且还需要把JWT的秘钥发过去,秘钥泄露的风险也会大大提高。所以,网关里设置登录校验是最理想的实现方式,而且JWT校验必须在网关将请求转发之前做,那么如何做呢?
网关请求处理流程——如何在网关转发之前做登录校验
网关的底层是没有业务逻辑的,它要做的事情就是基于配置的路由规则,判断前端的请求由哪个微服务处理,然后将请求转发到对应的微服务。
而这里对路由规则的判断就是由HandlerMapping
的接口(路由映射器)处理的。HandlerMapping的默认实现是RoutePredicateHandlerMapping
(路由断言),它会根据请求找到匹配的路由并存入上下文,然后把请求交给WebHandler
处理(这也是责任链模式)。
WebHandler是请求处理器,它的默认实现是FilteringWebHandler
(过滤器处理器)。它会加载网关中配置的多个过滤器,放入集合并排序,形成过滤器链,然后依次执行这个过滤器。
过滤器的最后是默认的NettyRoutingFilter
,Netty路由过滤器,负责将请求转发到微服务,当微服务返回结果后存入上下文。由此看出,请求转发到微服务之前和微服务处理完成之后都会调用。而过滤器内部都有PRE和POST两部分逻辑。
请求进来以后会先执行过滤器的PRE逻辑,如果PRE执行失败,那么会直接结束,不会再进行路由转发。
那么我们就可以自定义过滤器,并且保证这个过滤器在NettyRoutingFilter之前,然后在编写登录校验的逻辑,放在PRE阶段。
网关请求处理流程——网关如何将用户信息传递到微服务
网关是没有业务逻辑的,但是微服务需要得到用户的登录信息才能进行下一步业务,那么就有新的问题:网关如何将用户信息传递到微服务?
之前可以用ThreadLocal保存,但是每个微服务都会运行在不同的服务器上,而ThreadLocal只会保存在单独的JVM中,这显然不行。所以可以将用户信息保存在请求头中。
自定义过滤器
网关过滤器有两种,分别是
- GateWayFilter:路由过滤器,作用与任意指定的路由;默认不生效,要配置到路由才生效。
- GlobalFilter:全局过滤器,作用范围是所有路由;声明后自动生效。
定义一个类实现GlobalFilter接口,标注@Component定义为Bean,然后通过exchange.getRequest().getgetHeaders()
获取到请求头信息。
然后还需要将过滤器放在NettyRoutingFilter之前执行,那么就还需要实现Order
接口进行排序,实现getOrder方法后返回一个int值,这个值越小,执行优先级越高。所以我们需要return的值小于NettyRoutingFilter定义的值(Integer.MaxValue)。
配置管理
微服务重复配置过多,维护的成本高,需要给每个微服务都配置对应的yml文件。
业务配置经常变动,每次修改都需要查询编译,重启服务。
网关路由配置写死,如果变更则需要重启网关。
那么就需要将这些配置添加到Nacos中(建议全部添加)。
在命名空间中先新增命名空间,然后在配置管理-配置列表在自己的命名空间下新增配置。
其次还需要新建bootstarp.yaml文件,配置服务名和nacos地址等信息。
服务保护和分布式事务
雪崩问题
什么是雪崩
微服务调用链路中的某个服务故障,引起整个链路中的所有微服务都不可用,这就是雪崩。
雪崩产生的原因是什么
- 微服务之间相互调用,服务提供者出现故障或相互阻塞
- 服务调用者没有做好异常处理,导致自身故障
- 调用链中所有服务级联失败,导致整个集群故障。
解决问题的思路有哪些
- 尽量避免服务出现故障或阻塞
- 服务调用者要做好远程调用异常的后背方案(如果一旦出现异常,有相应的处理),避免故障扩散
服务保护方案——请求限流
请求限流:限制访问接口的请求的并发量,避免服务因流量激增而出现故障。
使用限流器来处理,然后缓慢的放行请求去访问接口(削峰填谷、流量整形)。
如果服务真的出现了故障,那么如何避免故障扩散?线程隔离
服务保护方案——线程隔离(服务降级)
线程隔离也叫作舱壁模式,模拟船舱挡板的放水原理。通过限定每个业务能使用的线程数量而将业务故障隔离,避免故障扩散。
服务保护方案——服务熔断(服务降级)
服务熔断:由断路器统计请求异常比例或慢调用比例,如果超出阈值则会熔断该业务,则拦截该接口的请求。熔断时期,所有请求快速失败,全部走fallback逻辑。
线程隔离、服务熔断统称为服务降级方案
Sentinel组件
上述所说的服务保护方案:请求限流、线程隔离和服务熔断都由Sentinel实现。它是阿里巴巴开源的一款微服务流量控制组件。
引入Sentinel依赖,配置Sentinel地址,然后只需要重启服务,即可在Sentinel客户端(8090端口)访问
簇点连读:就是单机调用链路。是一次请求进入服务后经过的每一个诶Sentinel监控的资源链。
默认情况下,Sentinel会监控SpringMVC的每一个Http接口。
限流、熔断等都是针对簇点链路中的资源设置的。而资源名默认就是接口的请求路径。
请求限流
在簇点链路后面点击流控button,即可对其做限流配置。其中QPS就是每秒的请求数量。
线程隔离
当一个微服务出现阻塞或者故障时,调用的接口可能会被拖慢,甚至资源耗尽。所以必须要限制查询接口的可用线程数,实现线程隔离。
首先让OpenFeign整合Sentinel:
feign:
sentinel:
enable:true #开启Feign对Sentinel的整合
然后在配置流控时选择并发线程数
Fallback
如果业务掉接口的速度非常慢,每次都好几百毫秒,我们应该让它熔断,走Fallback流程,快速失败,快速返回。
FeignClient的Fallback有两种配置方式:
- FallbackClass,无法对远程调用的异常做处理。
- FallbackFactory,可以对远程调用的异常做处理,通常都会选用这种。
步骤一:自定义类,实现FallbackFactory,编写某个FeignClient的fallback逻辑。
步骤二:将刚刚定义的UserClientFallbackFactory注册为一个Bean
步骤三:在UserClient接口中使用UserClientFallbackFactory
服务熔断
当请求走线程隔离的过程中,是会有几个线程先去执行接口,然后等待长时间发现不行才走fallback的,还是会有线程做无效等待,占用线程资源。我们希望看到的是你这个接口访问这么慢,下次就直接不去访问你,这就是熔断。
但是当接口恢复后,熔断是应该取消的,我们的断路器统计服务调用的异常比例,满请求比例,如果超出阈值则会熔断该服务,拦截一切该服务的请求。当服务恢复时,断路器会放行该服务的请求。
点击控制台中簇点资源的熔断按钮,即可配置熔断策略:
分布式事务
在分布式的系统中,如果一个业务需要多个服务合作完成,而且每个服务都有事物,多个事物必须同时成功或失败,这样的事物就是分布式事物
其中每个服务的事物就是应该分支事务,整个业务成为全局事务。
分布式事物的解决思路:解决分布式事务,各个子事务必须能感知到彼此的事务状态,才能实现状态一致。
Seata组件
Seata是阿里巴巴开源的分布式事物解决方案。
事物协调者:需要有外部协调者的参与,来确保每个子事务都可以感知到彼此的状态。
Seata事务管理中有三个重要角色:
- TC(事务协调者):维护全局和分布事务的状态,协调全局事务或回滚。
- TM(事务管理器):定义全局事务的范围,开始全局事务,提交或回滚全局事务。
- RM(资源管理器):管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态。