微服务—初入茅庐

张开发
2026/4/11 12:04:46 15 分钟阅读

分享文章

微服务—初入茅庐
目录1、微服务拆分远程调用2、服务治理注册中心服务注册服务发现3、OpenFeign快速入门连接池最佳实践日志4、网关网关路由网关登录校验网关过滤器登录校验微服务获取用户OpenFeign传递用户5、配置管理配置共享配置热更新6、微服务保护和分布式事务服务保护方案—请求限流服务保护方案—线程隔离服务保护方案—服务熔断服务保护方案—失败处理7、SentinelSentinel—FallbackSentinel—服务熔断8、分布式事务SeataXA模式AT模式1、微服务拆分什么时候拆分创业型项目采用单体架构开发、试错随着规模扩大逐渐拆分大型项目直接选择微服务架构避免后续拆分麻烦怎么拆分高内聚每个微服务的职责要尽量单一包含的业务相互关联度高、完整度高低耦合每个微服务的功能要相对独立减少对其它微服务的依赖1、纵向拆分按照业务模块拆分2、横向拆分抽取公共服务提高复用性工程结构为两种——独立Project——Maven聚合常见远程调用Spring提供了一个RestTemplate工具方便实现Http请求的发送使用步骤1、注入RestTemplate到Spring容器Bean public RestTemplate restTemplate() { return new RestTemlate(); }2、发起远程调用public T ResponseEntityT exchange( String url //请求路径 HttpMethod method, //请求方式 Nullable HttpEntity? requstEntity, //请求实体可以为空 ClassT responseType, //返回值类型 MapString,? uriVariables //请求参数 ​ ​ )2、服务治理注册中心服务治理的三个角色服务提供者暴露服务接口供其他服务调用服务消费者调用其它服务提供的接口注册中心记录并监控微服务各实例状态推送服务变更信息服务提供者会在启动时注册自己信息到注册中心消费者可以从注册中心订阅和拉取服务信息服务提供者通过心跳机制向注册中心报告自己的健康状态当心跳异常时注册中心会将异常服务剔除并通知订阅该服务的消费者当服务提供者有多个实例消费者可以通过负载均衡算法从多个实例选择服务注册服务注册步骤1、引入nacos discovery依赖!--nacos 服务注册发现-- dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency2、配置Nacos地址spring: application: name: item-service # 服务名称 cloud: nacos: server-addr: 192.168.150.101:8848 # nacos地址服务发现消费者需要连接nacos以拉取和订阅服务因此服务发现的前两步与服务注册一样后面加上服务调用即可1、引入nacos discovery依赖2、配置nacos地址3、服务发现3、OpenFeign快速入门OpenFeign是一个声明式的http客户端其作用是基于SpringMVC的常见注解帮我们实现http请求的发送OpenFeign已经被SpringCloud自动装配实现简单1、引入依赖包括OpenFeign和负载均衡组件SpringCloudLoadBalancer!--openFeign-- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-openfeign/artifactId /dependency !--负载均衡器-- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-loadbalancer/artifactId /dependency2、通过EnableFeignClients注解启用OpenFeign功能EnableFeignClients SpringBootApplication public class CartApplication{ ....}3、编写FeignClient这里只需要声明接口无需实现方法。接口中的几个关键信息FeignClient(item-service)声明服务名称GetMapping声明请求方式GetMapping(/items)声明请求路径RequestParam(ids) CollectionLong ids声明请求参数ListItemDTO返回值类型FeignClient(item-service) public interface ItemClient { ​ GetMapping(/items) ListItemDTO queryItemByIds(RequestParam(ids) CollectionLong ids); }4、使用FeignClient实现远程调用ListItemDTO items itemClient.queryItemByIds(List.of(1,2,3));有上述信息OpenFeign就可以利用动态代理帮我们实现这个方法特性原生方式RestTemplateOpenFeign代码风格命令式、模板代码多声明式、简洁优雅集成度需要手动集成负载均衡等与 Spring Cloud 生态无缝集成可维护性接口变更需修改多处调用代码接口即契约修改一处即可可读性业务逻辑与 HTTP 调用混杂远程调用透明化更关注业务配置能力分散、不易管理集中配置支持超时、重试、拦截器等连接池OpenFeign对Http请求做了优雅的伪装不过其底层发起http请求依赖于其他的框架.三种框架HttpURLConnection默认实现不支持连接池Apache HttpClient支持连接池OKHttp支持连接池使用带连接池可以减少创建连接的销毁连接的开销OpenFeign整合OKHttp的步骤如下1、引入依赖!--OK http 的依赖 -- dependency groupIdio.github.openfeign/groupId artifactIdfeign-okhttp/artifactId /dependency2、开启连接池功能feign: okhttp: enabled: true # 开启OKHttp功能最佳实践为了避免重复编码有两种思路解决方案1抽取相对简单工程结构清晰缺点是整个项目耦合度偏高日志OpenFeign默认只会在FeignClient所在包的日志级别为debug时输出日志其日志分为四级none不记录任何日志信息默认值basic仅记录请求的方法URL以及响应状态码和执行时间headers在basic的基础上额外记录了请求和响应的头信息pull记录素有请求和响应的明细包括头信息、请求体、元数据日志配置1、定义日志级别​public class DefaultFeignConfig { Bean public Logger.Level feignLogLevel(){ return Logger.Level.FULL; } }2、启动日志级别需要配置类有两种方式局部生效在某个FeignClient中配置只对当前FeignClient生效FeignClient(value item-service, configuration DefaultFeignConfig.class)全局生效在EnableFeignClients中配置针对所有FeignCLient生效EnableFeignClients(defaultConfiguration DefaultFeignConfig.class)4、网关网关Gateway就是网络的关口负责请求的路由、转发、身份校验顾名思义网关就是网络的关口。数据在网络间传输从一个网络传输到另一个网络就需要过网关来做数据的路由和转发以及安全校验在SpringCloud中网关实现包括两种1、Spring Cloud Gateway常用Spring官方出品基于WebFlux响应式编程无需调优即可获得优异性能2、Netfilx Zuul基于Servlet的阻塞时编程网关路由网关路由对应的java类型是RouteDefinition,常见的属性有id路由唯一表示uri路由目标地址predicates路由断言判断请求是否符合当前路由filters路由过滤器对请求或响应做特殊处理网关本身也是一个独立的微服务因此也需要一个模块开发功能。大概步骤如下创建网关微服务引入SPringCloudGateway、NacosDiscovery依赖编写启动类配置网关路由配置路由规则路由断言Spign提供了12种基本的RoutePredicateFactory实现路由过滤器网关中提供了33种路由过滤器每种过滤器都有独特的作用网关登录校验单体架构时我们只需要完成一次用户登录、身份校验就可以在所有业务中获取到用户信息。而微服务拆分后每个微服务都独立部署不再共享数据。也就意味着每个微服务都需要做登录校验显然不可取JWT算法复杂而且需要密钥如果每个微服务都做登录检验存在两大问题每个微服务都需要知道JWT的密钥不安全每个微服务重复编写登录校验代码、权限校验代码网关是所有微服务的入口一切请求都需要经过网关所有我们可把登录校验的工作放到网关只需要在网关和用户服务保存密钥只需要在网关开发登录校验功能网关过滤器登录校验必须在请求转发到微服务之前做否则失去意义。网关的请求转发是Gateway内部代码实现的要想在请求转发之前做登录校验就必须了解Gateway内部工作的基本原理如图所示客户端请求进入网关后由HandlerMapping对请求做判断找到与当前请求匹配的路由规则Route然后将请求交给WebHandler去处理。WebHandler则会加载当前路由下需要执行的过滤器链Filter chain然后按照顺序逐一执行过滤器后面称为Filter。图中Filter被虚线分为左右两部分是因为Filter内部的逻辑分为pre和post两部分分别会在请求路由到微服务之前和之后被执行。只有所有Filter的pre逻辑都依次顺序执行通过后请求才会被路由到微服务。微服务返回结果后再倒序执行Filter的post逻辑。最终把响应结果返回。如图中所示最终请求转发是有一个名为NettyRoutingFilter的过滤器来执行的而且这个过滤器是整个过滤器链中顺序最靠后的一个。如果我们能够定义一个过滤器在其中实现登录校验逻辑并且将过滤器执行顺序定义到NettyRoutingFilter之前这就符合我们的需求了网关过滤器链中的过滤器有两种GatewayFilter路由过滤器作用范围比较灵活可以任意指定的路由由RouteGlobalFilter全局过滤器作用范围是所在路由不可配置实现自定义过滤器1、2、自定义GlobalFilter比较简单直接实现GlobalFilter接口即可​ Ordered保证了自定义过滤器在NettyRoutingFilter之前执行登录校验AuthProperties配置登录校验需要拦截的路径因为不是所有的路径都需要登录才能访问JwtProperties定义与JWT工具有关的属性比如秘钥文件位置SecurityConfig工具的自动装配JwtToolJWT工具其中包含了校验和解析token的功能hmall.jks秘钥文件其中AuthProperties和JwtProperties所需的属性要在application.yaml中配置hm: jwt: location: classpath:hmall.jks # 秘钥地址 alias: hmall # 秘钥别名 password: hmall123 # 秘钥文件密码 tokenTTL: 30m # 登录有效期 auth: excludePaths: # 无需登录校验的路径 - /search/** - /users/login - /items/**package com.hmall.gateway.filter; ​ import com.hmall.common.exception.UnauthorizedException; import com.hmall.common.utils.CollUtils; import com.hmall.gateway.config.AuthProperties; import com.hmall.gateway.util.JwtTool; import lombok.RequiredArgsConstructor; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; ​ import java.util.List; ​ Component RequiredArgsConstructor EnableConfigurationProperties(AuthProperties.class) public class AuthGlobalFilter implements GlobalFilter, Ordered { ​ private final JwtTool jwtTool; ​ private final AuthProperties authProperties; ​ private final AntPathMatcher antPathMatcher new AntPathMatcher(); ​ Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 1.获取Request ServerHttpRequest request exchange.getRequest(); // 2.判断是否不需要拦截 if(isExclude(request.getPath().toString())){ // 无需拦截直接放行 return chain.filter(exchange); } // 3.获取请求头中的token String token null; ListString headers request.getHeaders().get(authorization); if (!CollUtils.isEmpty(headers)) { token headers.get(0); } // 4.校验并解析token Long userId null; try { userId jwtTool.parseToken(token); } catch (UnauthorizedException e) { // 如果无效拦截 ServerHttpResponse response exchange.getResponse(); response.setRawStatusCode(401); return response.setComplete(); } ​ // TODO 5.如果有效传递用户信息 System.out.println(userId userId); // 6.放行 return chain.filter(exchange); } ​ private boolean isExclude(String antPath) { for (String pathPattern : authProperties.getExcludePaths()) { if(antPathMatcher.match(pathPattern, antPath)){ return true; } } return false; } ​ Override public int getOrder() { return 0; } }微服务获取用户现在网关已经可以完成登录校验并获取登录用户身份信息。但是当网关将请求转发到微服务时微服务又该如何获取用户身份呢由于网关发送请求到微服务依然采用的是Http请求因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。考虑到微服务内部可能很多地方都需要用到登录用户信息因此我们可以利用SpringMVC的拦截器来实现登录用户信息获取并存入ThreadLocal方便后续使用。据图流程图如下因此接下来我们要做的事情有改造网关过滤器在获取用户信息后保存到请求头转发到下游微服务编写微服务拦截器拦截请求获取用户信息保存到ThreadLocal后放行修改网关过滤器步骤1、保存用户到请求头接下来我们只需要编写拦截器获取用户信息并保存到UserContext然后放行即可。由于每个微服务都有获取登录用户的需求因此拦截器我们直接写在hm-common中并写好自动装配。这样微服务只需要引入hm-common就可以直接具备拦截器功能无需重复编写。2、编写拦截器package com.hmall.common.interceptor; ​ import cn.hutool.core.util.StrUtil; import com.hmall.common.utils.UserContext; import org.springframework.web.servlet.HandlerInterceptor; ​ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; ​ public class UserInfoInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.获取请求头中的用户信息 String userInfo request.getHeader(user-info); // 2.判断是否为空 if (StrUtil.isNotBlank(userInfo)) { // 不为空保存到ThreadLocal UserContext.setUser(Long.valueOf(userInfo)); } // 3.放行 return true; } ​ Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 移除用户 UserContext.removeUser(); } }3、接着编写SpringMVC的配置类配置登录拦截器package com.hmall.common.config; ​ import com.hmall.common.interceptors.UserInfoInterceptor; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; ​ Configuration ConditionalOnClass(DispatcherServlet.class) public class MvcConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserInfoInterceptor()); } } 不过需要注意的是这个配置类默认是不会生效的因为它所在的包是com.hmall.common.config与其它微服务的扫描包不一致无法被扫描到因此无法生效。 基于SpringBoot的自动装配原理我们要将其添加到resources目录下的META-INF/spring.factories文件中 org.springframework.boot.autoconfigure.EnableAutoConfiguration\ com.hmall.common.config.MyBatisConfig,\ com.hmall.common.config.MvcConfigOpenFeign传递用户前端发起的请求都会经过网关再到微服务由于我们之前编写的过滤器和拦截器功能微服务可以轻松获取登录用户信息。但有些业务是比较复杂的请求到达微服务后还需要调用其它多个微服务。比如下单业务流程如下下单的过程中需要调用商品服务扣减库存调用购物车服务清理用户购物车。而清理购物车时必须知道当前登录的用户身份。但是订单服务调用购物车时并没有传递用户信息购物车服务无法知道当前用户是谁由于微服务获取用户信息是通过拦截器在请求头中读取因此要想实现微服务之间的用户信息传递就必须在微服务发起调用时把用户信息存入请求头。微服务之间调用是基于OpenFeign来实现的并不是我们自己发送的请求。我们如何才能让每一个由OpenFeign发起的请求自动携带登录用户信息呢这里要借助Feign中提供的一个拦截器接口feign.RequestInterceptorpublic interface RequestInterceptor { ​ /** * Called for every request. * Add data using methods on the supplied {link RequestTemplate}. */ void apply(RequestTemplate template); } ​我们只需要实现这个接口然后实现apply方法利用RequestTemplate类来添加请求头将用户信息保存到请求头中。这样以来每次OpenFeign发起请求的时候都会调用该方法传递用户信息Bean public RequestInterceptor userInfoRequestInterceptor(){ return new RequestInterceptor() { Override public void apply(RequestTemplate template) { // 获取登录用户 Long userId UserContext.getUser(); if(userId null) { // 如果为空则直接跳过 return; } // 如果不为空则放入请求头中传递给下游微服务 template.header(user-info, userId.toString()); } }; }现在微服务之间通过OpenFeign调用也会传递登录用户信息了5、配置管理微服务重复配置过多维护成本高业务配置经常变动每次修改都要重启服务网关路由配置写死如果变更要重启网关配置共享我们可以把微服务共享的配置抽取到Nacos中统一管理这样就不需要每个微服务都重新配置了1、在Nacos中添加共享配置2、微服务拉取配置第一步注意这里的jdbc的相关参数并没有写死例如数据库ip通过${hm.db.host:192.168.150.101}配置了默认值为192.168.150.101同时允许通过${hm.db.host}来覆盖默认值数据库端口通过${hm.db.port:3306}配置了默认值为3306同时允许通过${hm.db.port}来覆盖默认值数据库database可以通过${hm.db.database}来设定无默认值第二步拉取共享配置接下来我们要在微服务拉取共享配置。将拉取到的共享配置与本地的application.yaml配置合并完成项目上下文的初始化。不过需要注意的是读取Nacos配置是SpringCloud上下文ApplicationContext初始化时处理的发生在项目的引导阶段。然后才会初始化SpringBoot上下文去读取application.yaml。也就是说引导阶段application.yaml文件尚未读取根本不知道nacos 地址该如何去加载nacos中的配置文件呢SpringCloud在初始化上下文的时候会先读取一个名为bootstrap.yaml(或者bootstrap.properties)的文件如果我们将nacos地址配置到bootstrap.yaml中那么在项目引导阶段就可以读取nacos中的配置了。微服务整合Nacos配置管理步骤如下1、引入依赖!--nacos配置管理-- dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-config/artifactId /dependency !--读取bootstrap文件-- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-bootstrap/artifactId /dependency2、新建bootstrap.ymalspring: application: name: cart-service # 服务名称 profiles: active: dev cloud: nacos: server-addr: 192.168.150.101 # nacos地址 config: file-extension: yaml # 文件后缀名 shared-configs: # 共享配置 - dataId: shared-jdbc.yaml # 共享mybatis配置 - dataId: shared-log.yaml # 共享日志配置 - dataId: shared-swagger.yaml # 共享日志配置记得修改yaml不必要的配置配置热更新当修改配置文件中的配置时微服务无需重启即可配置生效前提条件1、nacos中要有一个与微服务名有关的配置文件配置yaml文件2、微服务中要以特定方式读取需要热更新的配置属性6、微服务保护和分布式事务雪崩问题微服务调用链路中的某个服务故障引起整个链路中的所有微服务都不可用这就是雪崩雪崩产生的原因微服务相互调用服务提供者出现故障或阻塞服务调用者没有做好异常处理导致自身故障调用链中的所有服务级联失败导致整个集群故障解决雪崩思路避免服务出现故障或阻塞能应对高并发请求服务保护方案—请求限流请求限流限制访问微服务的请求的并发量避免服务因流量激增出现故障服务保护方案—线程隔离线程隔离也叫舱壁模式模拟船舱隔板防水原理。通过限定每个业务能使用的线程数量而将故障业务隔离。避免故障扩散服务保护方案—服务熔断服务熔断由断路器统计请求的异常比例或慢调用比例如果超出阈值则会熔断该业务则拦截该接口的请求。熔断期间所有请求快速失败全都走fallback逻辑服务保护方案—失败处理失败处理定义fallback逻辑让业务失败时不再抛出异常而是返回默认数据或友好提示7、SentinelSentinel是阿里巴巴开源的一款微服务流量控制组件簇点链路就是单机调用链路是一次请求进入服务后经过的每一个Sentinel监控的资源链。默认Sentinel会监控SpringMVC的每一个HTTP接口限流、熔断等都是针对簇点链路中的资源设置Sentinel—Fallback实现步骤1、将FeignClient作为Sentinel的簇点资源2、FeignClient的Fallback有两种配置方式FallbackClass无法对远程调用的异常做处理FallbackFactory可以对远程调用的异常做处理通常都会选择这种3、自定义类实现FallbackFactory,编写对某个FeignClient的fallback逻辑4、将刚刚定义的UserClientFallbackFactory注册为一个Bean5、在UserClient接口中使用UserClientFallbackFactorySentinel—服务熔断熔断是解决雪崩问题的重要手段思路是由断路器统计服务调用的异常比例、慢请求比例如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求当服务恢复时断路器会放行访问该服务的请求断路器的三个状态ClosedOpenHalf-Open8、分布式事务在分布式系统中如果一个业务需要多个服务合作完成而且每一个服务都有事务多个事务必须同时成功或失败这样的事务就是分布式事务其中每个服务的事务就是一个分支事务整个业务称为全局事务SeataSeata是19年开源的分布式事务解决方案致力于提供高性能和简单易用的分布式事务服务为用户打造一站式的分布式解决方案Seata架构Seata事务管理中有三个重要角色TC—事务协调者维护全局和分支事务的状态协调全局事务提交或回滚TM—事务管理器定义全局事务的范围、开机全局事务、提交或回滚全局事务RM—资源管理器管理分支事务与TC交谈以注册分支事务和报告分支事务的状态XA模式XA规模是X/Open组织定义的分布式事务处理标准XA规范描述了全局TM与局部的RM之间的接口几乎所有主流的关系型数据库都对XA规范提供了支持一阶段工作RM注册分支事务到TCRM执行分支业务SQL但不提交RM报告执行状态到TC二阶段工作TC检测各分支事务执行状态1、成功通知所有RM提交事务2、失败通知所有RM回滚事务RM接收TC指令提交或回滚事务实现XA模式Seata的starter已经完成了XA模式的自动装配实现简单1、修改application.yma文件每个参与事务的微服务开启XA模式2、给发起全局事务的入口方法添加GlobalTransactional注解3、重启测试AT模式Seata主推的是AT模式AT模式同样是分阶段提交的事务模型不过弥补了XA模型中资源锁定周期过长的缺陷阶段一RM的工作注册分支事务记录undo-log数据快照执行业务SQL并提交报告事务状态阶段二提交时RM的工作删除undo-log即可阶段二回滚时RM的工作根据undo-log恢复数据到更新前AT模式与XA模式的区别XA模式一阶段不提交事务锁定资源AT模式一阶段直接提交不锁定资源XA模式依赖数据库机制实现回滚AT模式利用数据快照实现数据回滚可见AT模式使用更加简单无业务侵入性能更好

更多文章