微服务SpringCloudAlibaba
微服务拆分
微服务拆分的核心是按业务边界解耦,将单体应用拆分为独立运行的服务。
- 原则:遵循 “单一职责”,每个服务专注于特定业务域(如订单、商品、用户),做到高内聚低耦合。
- 目的:便于团队独立开发、部署和扩展,避免单体应用的 “牵一发而动全身”。
- 关键:拆分粒度需平衡(过细会增加服务通信成本,过粗则失去微服务优势),需考虑服务间依赖关系。
NACOS
在微服务远程调用的过程中,包括两个角色:
- 服务提供者:提供接口供其它微服务访问,比如
item-service
- 服务消费者:调用其它微服务提供的接口,比如
cart-service
在大型微服务项目中,服务提供者的数量会非常多,为了管理这些服务就引入了注册中心的概念。注册中心、服务提供者、服务消费者三者间关系如下:
流程如下:
- 服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
- 调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
- 调用者自己对实例列表负载均衡,挑选一个实例
- 调用者向该实例发起远程调用
当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?
- 服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)
- 当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除
- 当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
- 当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表
我们项目使用阿里的NACOS,用来注册服务,只要给我们的微服务引入依赖和配置,就可以在注册中心看到微服务,目前我们只使用了nacos的配置中心,可以将几个微服务公共的yaml写入配置列表
OpenFeign
OpenFeign 是声明式 HTTP 客户端,简化微服务间远程调用。
- 基于接口 + 注解定义调用规则(如
@FeignClient
指定目标服务),无需手动编写 HTTP 请求代码。 - 自动集成 Ribbon,实现负载均衡(从 Nacos 获取服务列表后,分发请求到不同实例)。
- 支持整合 Sentinel,为远程调用添加熔断、降级能力(如配置
fallback
处理调用失败场景)。
项目中使用OpenFeign主要是定义了一个api模块,将一些多个微服务都要调用的接口或者方法,放在api模块中通过在微服务启动类@EnableFeignClients,在Client中使用@FeignClient(“item-service”)注解,使用远程调用服务,同时我们配置了okHttp替换默认的HttpURLConnection,支持连接池。
支持日志输出需要创建一个配置类注册bean,在需要打印日志的client加上
1 |
1 | //启动类配置 全局生效 |
1 | package com.hmall.api.config; |
Gateway
Gateway其实就是一个网关微服务,我们定义一个新的微服务,在项目中我们快速入门是使用hm-gateway完成了路由转发,nginx统一请求8080,我们通过gateway,将进入8080的请求,通过路由规则转发给对应的目标服务,同时它还支持断言和路由过滤、动态路由等作用。总结:Gateway 是微服务的统一入口网关,处理所有外部请求,核心功能是路由与过滤。
1 | server: |
网关登录校验
单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。而微服务拆分后,每个微服务都独立部署,不再共享数据。也就意味着每个微服务都需要做登录校验,这显然不可取。
我们的登录是基于JWT来实现的,校验JWT的算法复杂,而且需要用到秘钥。如果每个微服务都去做登录校验,这就存在着两大问题:
- 每个微服务都需要知道JWT的秘钥,不安全
- 每个微服务重复编写登录校验代码、权限校验代码,麻烦
既然网关是所有微服务的入口,一切请求都需要先经过网关。我们完全可以把登录校验的工作放到网关去做,这样之前说的问题就解决了:
- 只需要在网关和用户服务保存秘钥
- 只需要在网关开发登录校验功能
- 网关路由是配置的,请求转发是Gateway内部代码,我们如何在转发之前做登录校验?
- 网关校验JWT之后,如何将用户信息传递给微服务?
- 微服务之间也会相互调用,这种调用不经过网关,又该如何传递用户信息?
首先登录校验必须在请求转发到微服务之前做。而网关的请求转发是Gateway
内部代码实现的,要想在请求转发之前做登录校验,就必须了解Gateway
内部工作的基本原理。
- 客户端请求进入网关后由
HandlerMapping
对请求做判断,找到与当前请求匹配的路由规则(Route
),然后将请求交给WebHandler
去处理。 WebHandler
则会加载当前路由下需要执行的过滤器链(Filter chain
),然后按照顺序逐一执行过滤器(后面称为**Filter
**)。- 图中
Filter
被虚线分为左右两部分,是因为Filter
内部的逻辑分为pre
和post
两部分,分别会在请求路由到微服务之前和之后被执行。 - 只有所有
Filter
的pre
逻辑都依次顺序执行通过后,请求才会被路由到微服务。 - 微服务返回结果后,再倒序执行
Filter
的post
逻辑。 - 最终把响应结果返回。
如图中所示,最终请求转发是有一个名为NettyRoutingFilter
的过滤器来执行的,而且这个过滤器是整个过滤器链中顺序最靠后的一个。如果我们能够定义一个过滤器,在其中实现登录校验逻辑,并且将过滤器执行顺序定义到**NettyRoutingFilter
**之前,这就符合我们的需求了。
网关过滤器链中的过滤器有两种:
GatewayFilter
:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route
.GlobalFilter
:全局过滤器,作用范围是所有路由,不可配置。
GatewayFilter
该过滤器实现,需要先创建一个类继承AbstractGatewayFilterFactory,然后重写Apply方法。还需要在对应的application文件中进行配置,注意!该类的名称一定要以GatewayFilterFactory为后缀。在yaml需要配置,可以全局也可以单个服务。
1 | spring: |
我这里测试直接使用了全局过滤器。
1 |
|
GlobalFilter
这个是在项目中使用的过滤器,他使用比较简答其实,不支持动态参数,因为项目只做一个过滤并且将用户信息传参,所以就用GlobalFilter,他只要实现两个接口,一个GlobalFilter,一个ordered(排序的可以不给)。ordered可以设置过滤器的执行顺序,按照自己业务情况来。因为项目中使用的这个,测试用例就不写了。
登录校验
这里我们要先把配置类写好在gateway服务中,创建过滤器,当然秘钥和校验路径要在yaml配置好。exchange中有请求头信息,通过chain转发给其他服务。
1 |
|
用户信息传递
现在我们把用户ID发送到了请求头中并且命名为user-info,那我们再定义一个拦截器,将请求头中的用户id拿到放到threadlocal里面就可以了。下面是流程图。
由于每个微服务都有获取登录用户的需求,因此拦截器我们直接写在hm-common
中,并写好自动装配。这样微服务只需要引入hm-common
就可以直接具备拦截器功能,无需重复编写。在hm-common定义一个拦截器实现HandlerInterceptor接口。
1 | public class UserInfoInterceptor implements HandlerInterceptor { |
使用到了这个接口就讲一下把。
HandlerInterceptor
接口
这是 Spring MVC 提供的拦截器接口,用于在控制器(Controller)方法执行前后插入自定义逻辑,主要有三个方法:
preHandle
:请求到达控制器之前执行(预处理)postHandle
:控制器方法执行完成但视图未渲染之前执行(后处理)afterCompletion
:整个请求完成(包括视图渲染)之后执行(资源清理)
同时这里我们要引入一个springMvc的配置类,配置刚刚的拦截器。
1 |
|
这里并不会直接生效,因为其他微服务的并不会扫描到这个包,基于SpringBoot的自动装配原理,我们要将其添加到resources
目录下的META-INF/spring.factories
文件中。这里我后面会复习自动装配的原理的。这个时候threadlocal里面就有我们存入的用户信息了。
OpenFeign传递用户
还有存在一个问题,就是当我们下单服务的时候,订单服务获取到了用户信息保存订单,但是到清理购物车的到时候,在购物车服务是没有当前用户的ID的,就会导致购物车服务不知道删除哪个用户的购物车。注意因为是不同的服务,所以不是同一个线程也就不是同一个threadlocal。由于微服务获取用户信息是通过拦截器在请求头中读取,因此要想实现微服务之间的用户信息传递,就必须在微服务发起调用时把用户信息存入请求头。因为我们是使用OpenFeign实现微服务之间互相调用的,我们可以将OpenFeign每次的发起请求都携带登录用户信息就好了。
Feign有一个拦截器接口feign.RequestInterceptor,只要实现这个接口并实现apply方法就行了,利用RequestTemplate
类来添加请求头,将用户信息保存到请求头中。可以在之前api模块中DefaultFeignClient里面注册一个方法bean。这样就完成了通过gateway实现服务之前通讯啦!!!
1 |
|
配置管理
后面的配置管理其实就是公共yaml的配置,通过定义一个属性类@ConfigurationProperties(prefix = “hm.cart”)可以实现热更新 。后面放一个动态路由的配置代码。具体就不讲了,网上教程比较多。
1 | package com.hmall.gateway.routers; |
微服务保护(Sentinel)
我们可以通过阿里开发的Sentinel实现微服务的保护,它里面自带qps实时监控和直接对请求接口簇点链路进行熔断和流控以及隔离。
报错±-add-opens=java.base/java.lang=ALL-UNNAMED
请求限流
服务故障最重要原因,就是并发太高!解决了这个问题,就能避免大部分故障。当然,接口的并发不是一直很高,而是突发的。因此请求限流,就是限制或控制接口访问的并发流量,避免服务因流量激增而出现故障。
请求限流往往会有一个限流器,数量高低起伏的并发请求曲线,经过限流器就变的非常平稳。这就像是水电站的大坝,起到蓄水的作用,可以通过开关控制水流出的大小,让下游水流始终维持在一个平稳的量。
项目:我们这里直接对购物车设置qps为10,每秒发100个请求,发现拒绝qps90,通过10。实现限流
线程隔离
当一个业务接口响应时间长,而且并发高时,就可能耗尽服务器的线程资源,导致服务内的其它接口受到影响。所以我们必须把这种影响降低,或者缩减影响的范围。线程隔离正是解决这个问题的好办法。t
轮船的船舱会被隔板分割为N个相互隔离的密闭舱,假如轮船触礁进水,只有损坏的部分密闭舱会进水,而其他舱由于相互隔离,并不会进水。这样就把进水控制在部分船体,避免了整个船舱进水而沉没。为了避免某个接口故障或压力过大导致整个服务不可用,我们可以限定每个接口可以使用的资源范围,也就是将其“隔离”起来。
如图所示,我们给查询购物车业务限定可用线程数量上限为20,这样即便查询购物车的请求因为查询商品服务而出现故障,也不会导致服务器的线程资源被耗尽,不会影响到其它接口。因为我们是FeignClien接口,所以项目中进行隔离的时候要开启Feign的Sentinel功能支持。
项目:我们对查询商品接口进行流控,因为查询购物车的下级链路是查询商品。
服务熔断
线程隔离虽然避免了雪崩问题,但故障服务(商品服务)依然会拖慢购物车服务(服务调用方)的接口响应速度。而且商品查询的故障依然会导致查询购物车功能出现故障,购物车业务也变的不可用了。
所以,我们要做两件事情:
- 编写服务降级逻辑:就是服务调用失败后的处理逻辑,根据业务场景,可以抛出异常,也可以返回友好提示或默认数据。
- 异常统计和熔断:统计服务提供方的异常比例,当比例过高表明该接口会影响到其它服务,应该拒绝调用该接口,而是直接走降级逻辑。
项目:这里我们用了FallbackFactory给FeignClient编写降级逻辑,有异常的时候返回空数据。
分布式事务(Seata)
在我们购物车提交订单业务中,在原本的单体项目中,我通过在提交订单方法中@Transactional,保证了事务的一致性。防止提交订单的时候购物车删除商品,但是在业务执行完成之前发生了报错(模拟库存没了),没有进行数据的回滚,导致购物车数据消失了而且没提交订单的情况。注册为单体事务,会保证ACID特性。
由于订单、购物车、商品分别在三个不同的微服务,而每个微服务都有自己独立的数据库,因此下单过程中就会跨多个数据库完成业务。而每个微服务都会执行自己的本地事务:
- 交易服务:下单事务
- 购物车服务:清理购物车事务
- 库存服务:扣减库存事务
整个业务中,各个本地事务是有关联的。因此每个微服务的本地事务,也可以称为分支事务。多个有关联的分支事务一起就组成了全局事务。我们必须保证整个全局事务同时成功或失败,每一个分支事务就是传统的单体事务,都可以满足ACID特性,经过测试在微服务中无法满足整个业务流程的ACID,参与事务的多个子业务在不同的微服务,跨越了不同的数据库。虽然每个单独的业务都能在本地遵循ACID,但是它们互相之间没有感知,不知道有人失败了,无法保证最终结果的统一,也就无法遵循ACID的事务特性了。
这就是分布式事务问题,出现以下情况之一就可能产生分布式事务问题:
- 业务跨多个服务实现
- 业务跨多个数据源实现
Seata
这里采用使用阿里开源的Seata解决分布式的事务问题。分布式事务产生的根本原因就是互相没感知么,那解决思路就像之前学Redis分布式锁一样,找一个能管理所有单体事务的东西。这里事务协调者,他的任务就是监控每个单体事务,判断事务提交和失败,然后找一个全局事务,保证全局事务下每个事务都要一起提交或者回滚。Seata也是这么个思路。
他分了TM,TC,RM。三个角色管理事务。
- TC(Transaction Coordinator)-事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
- TM (Transaction Manager)-事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
- RM (Resource Manager)-资源管理器:管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
其中,TM和RM可以理解为Seata的客户端部分,引入到参与事务的微服务依赖中即可。将来TM和RM就会协助微服务,实现本地分支事务与TC之间交互,实现事务的提交或回滚。
而TC服务则是事务协调中心,是一个独立的微服务,需要单独部署。部署过程就省略了,这里直接说一下Seata的两种分布式解决方案,AT和XA。一共有四种还有TCC和SAGA
XA模式
这种模式是强一致性事务,分两阶段的操作。首先TM会开启全局事务并管理分支事务
RM一阶段操作
- 注册分支事务到TC
- 执行业务sql但是不提交
- 报告事务的状态成功或失败
TC和RM二阶段操作
- TC检查各分支的状态,全成功就提交,有一个失败就回滚,通知RM执行操作
- 听到TC检查分支事务的状态决定是否全部提交
1 | seata: |
优点:
- 事务的强一致性,保证了ACID原则
- 常用数据库都支持,实现简单,没有代码入侵@GlobalTransaction
缺点:
- 类似于串行操作,当二阶段没结束的时候,一阶段会锁定数据库资源,RM管理的
微服务无法再执行其他操作
,当二阶段执行完毕以后才会释放一阶段的资源,会导致性能过差。
AT模式(默认)
AT
模式同样是分阶段提交的事务模型,不过缺弥补了XA
模型中资源锁定周期过长的缺陷。他的工作原理是这样的前面部分一样,但是在RM执行sql 的时候会直接提交**(这个时候会释放数据库锁),那如何进行数据回滚呢,他采用了快照,要使用AT模式,要在每个数据库建一个undo log表,他会在执行sql前记录更新前的数据,放到undo log表中,当后期TC通知RM提交的时候会删除log,如果要回滚,就按照记录的log进行数据回滚,回滚以后再删除数据,它是最终一致性**事务。这其实就是空间换时间的一种思想,保证直接提交,那么如果数据要回滚,就要记录数据。但是正常业务中其实回滚的操作是比较少的,所以用AT模式性能比较好,综合下来也比较好用,它也有缺点
——(假设这次会失败回滚)当分支业务提交以后~回滚之前,这段时间有其他业务访问数据库,会出现数据库不一致的问题,这是无法避免的,但是这段时间很短,要作取舍。
区别
XA
模式一阶段不提交事务,锁定资源;AT
模式一阶段直接提交,不锁定资源。XA
模式依赖数据库机制实现回滚;AT
模式利用数据快照实现数据回滚。XA
模式强一致;AT
模式最终一致