一下内容为本人在听黑马程序员的课程时整理的
⎛⎝≥⏝⏝≤⎛⎝ ⎛⎝≥⏝⏝≤⎛⎝ ⎛⎝≥⏝⏝≤⎛⎝ ⎛⎝≥⏝⏝≤⎛⎝
1、微服务框架
1.1、认识微服务
1.1.1、服务架构演变
**单体架构:**将业务的所有功能集中在一个项目中开发,打包成一个包部署
优点:
- 架构简单
- 部署成本低
缺点:
- 耦合度高
**分布式架构:**根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务
优点:
- 降低服务耦合
- 有利于服务升级扩展
服务治理:
分布式架构要考虑的问题:
- 服务拆分力度如何?
- 服务集群地址如何维护?
- 服务之间如何实现远程调用?
- 服务健康如何感知?
微服务
微服务是一种经过良好架构设计的分布式架构方案,微服务架构特征:
- 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
- 面向服务:微服务对外暴露业务接口
- 自治:团队独立、技术独立、数据独立、部署独立
- 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
微服务结构:
微服务这种方案需要技术框架来落地,全球的互联网公司都在积极尝试自己的微服务落地技术。在国内最知名的就是SpringCloud和阿里巴巴的Dubbo
1.1.2、SpringCloud
SpringCloud
- SpringCloud是目前国内使用最广泛的微服务框架
- SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验
- SpringCloud与SpringBoot的版本兼容关系如下:
1.2、服务拆分即远程调用
服务拆分注意事项:
- 不同微服务,不要重复开发相同业务
- 微服务数据独立,不要访问其它微服务的数据库
- 微服务可以将自己的业务暴露为接口,供其它服务调用
工程结构有两种:
- 独立Project
- Maven聚合
案例:拆分服务
- 将hm-service中与商品管理相关功能拆分到一个微服务module中,命名为item-service
- 将hm-service中与购物车有关的功能拆分到一个微服务module中,命名为cart-service
远程调用
![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=C%3A%5CUsers%5C13478%5CDesktop%5C%E8%87%AA%E5%AD%A6%E6%88%90%E6%89%8D%5CJavaWeb%5C%E5%BE%AE%E6%9C%8D%E5%8A%A1%E5%BC%
Spring给我们提供了一个RestTemplate工具,可以方便的实现Http请求的发送。使用步骤如下:
1、注入RestTemplate到Spring容器
@Bean
public RestTemplate restTemplate(){return new RestTemplate();
}
2、发起远程调用
public <T> ResponseEntity<T> exchange(Sring url, //请求路径HttpMethod method, //请求方式@Nullable HttpEntity<?> requestEntity, //请求实体,可以为空Class<T> responseType, //返回值类型Map<String,?> urlVariables //请求参数
)
1.3、服务治理
1.3.1、注册中心
服务治理中的三个角色分别是什么?
- 服务提供者:暴露服务接口,供其它服务调用
- 服务消费者:调用其他服务提供的接口
- 注册中心:记录并监控微服务各实例状态,推送服务变更信息
消费者如何知道提供者的地址号?
- 服务提供者会在启动时注册自己信息到注册中心,消费者可以从注册中心订阅和拉取服务信息
消费者如何得知服务状态变更?
- 服务提供者通过心跳机制向注册中心报告自己的健康状态,当心跳异常时注册中心会将异常服务剔除,并通知订阅了该服务的消费者
当提供者有多个实例时,消费者应该选择哪一个?
- 消费者可以通过负载均衡算法,从多个实例中选择一个
1.3.2、Nacos注册中心
Nacos是目前国内企业中占比最多的注册中心组件。它是阿里巴巴的产品,目前已经加入SpringCloudAlibaba中
部署Nacos:day03-微服务01 - 飞书云文档 (feishu.cn)
1.3.3、服务注册
服务注册步骤:
1、引入nacos discovery依赖:
<!--nacos 服务注册发现-->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2、配置nacos
cloud:
nacos:discovery:server-addr: 192.168.88.132:8848
1.3.4、服务发现
消费者需要连接nacos以拉取和订阅服务,因此服务发现的前两步与服务注册时一样的,后面再加上服务调用即可:
- 引入nacos discovery依赖
- 配置nacos地址
- 服务发现
private final DiscoveryClient discoveryClient;private void handleCartItems(List<CartVO> vos){//1.根据服务名称,拉取服务的实例列表List<ServiceInstance> instances = discoveryClient.getInstances("item-service");//2.负载均衡,挑选一个实例ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));//3.获取实例的IP和端口URU uri = instance.getUri();//....
}
1.4、OpenFeign
1.4.1、快速入 门
我们利用Nacos实现了服务的治理,利用RestTemplate实现了服务的远程调用。但是远程调用的代码太复杂了:
而且这种调用方式,与原本的本地方法调用差异太大,编程时的体验也不统一,一会儿远程调用,一会儿本地调用。
因此,我们必须想办法改变远程调用的开发模式,让远程调用像本地方法调用一样简单。而这就要用到OpenFeign组件了。
其实远程调用的关键点就在于四个:
- 请求方式
- 请求路径
- 请求参数
- 返回值类型
OpenFeign是一个声明式的http客户端,是SpringCloud在Eureka公司开源的Feign基础上改造而来的
其作用就是基于SpringMVC的常见注解,帮我们优雅的实现http请求的发送
OpenFeign已经被SpringCloud自动装配,实现非常简单
1、引入依赖
<!--openFeign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!--负载均衡器--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency>
2、通过@EnableFeignClients注解,启用OpenFeign功能
@EnabeleFeignClients
@SpringBootApplication
public class CartApplication{ /....}
3、编写FeignClient
@FeignClient("item-service")
public interface ItemClient {@GetMapping("/items")List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
4、使用FeignClient,实现远程调用
private void handleCartItems(List<CartVO> vos) {// 1.获取商品idSet<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());//2.查询商品List<ItemDTO> items = itemClient.queryItemByIds(itemIds);if (CollUtils.isEmpty(items)){return;}// 3.转为 id 到 item的mapMap<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));// 4.写入vofor (CartVO v : vos) {ItemDTO item = itemMap.get(v.getItemId());if (item == null) {continue;}v.setNewPrice(item.getPrice());v.setStatus(item.getStatus());v.setStock(item.getStock());}}
1.4.2、连接池
OpenFeign对Http请求做了优雅的伪装,不过其底层发起http请求,依赖于其他的框架。这些框架可以自己选择,包括一下三种:
- HttpURLConnection:默认实现,不支持连接池
- Apache HttpClient:支持连接池
- OKHttp:支持连接池
OpenFeign整合OKHttp的步骤:
1、引入依赖
<!--OK http 的依赖 -->
<dependency><groupId>io.github.openfeign</groupId><artifactId>feign-okhttp</artifactId>
</dependency>
2、开启连接池功能
feign:okhttp:enabled: true # 开启OKHttp功能
1.4.3、最佳实践
当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。有两种解决方法:
方式一:指定FeignClient所在的包
@EnableFeignClients(basePackages = "com.hmall.api.client")
方式二:指定FeignClient字码节
@EnableFeignClients(clients={UserClient.class})
1.4.4、日志
OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:
- NONE:不记录任何日志信息,这是默认值
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据
由于Feign默认的日志级别就是NONE,所以默认我们看不到请求日志
要自定义级别需要声明一个类型为Logger.Level的Bean,在其中定义日志级别:
public class DefaultFeignConfig{@Beanpublic Logger.Level feignLogLevel(){return Logger.Level.FULL;}
}
但此时这个Bean并未生效,想要配置某个FeignClient的日志,可以在@FeignClient注解中声明
@FeignClient(value = "item-service",configuration = DefaultFeignConfig.class)
如果想要全局配置,让所有的FeignClient都按照这个日志配置,则需要再@EnableFeignClients注解中声明:
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)
2、微服务-网关及配置管理
2.1、网关
网关就是网络的开关,负责请求的路由、转发、身份校验
在SpringCloud中网关的实现包括两种:
1、Spring Cloud Gateway
- Spring官方出品
- 基于WebFlux响应式编程
- 无需调优即可获得优异性能
2、Netfilx Zuul
- Netfilx出品
- 基于Servlet的阻塞式编程
- 需要调优才能获得与SpringCloudGateway类似的性能
2.2、网关路由
2.2.1、快速入门
1、创建新模块
2、引入网关依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>hmall</artifactId><groupId>com.heima</groupId><version>1.0.0</version></parent><modelVersion>4.0.0</modelVersion><artifactId>hm-gateway</artifactId><properties><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target></properties><dependencies><!--common--><dependency><groupId>com.heima</groupId><artifactId>hm-common</artifactId><version>1.0.0</version></dependency><!--网关--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><!--nacos discovery--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--负载均衡--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency></dependencies><build><finalName>${project.artifactId}</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
3、编写启动类
@SpringBootApplication
public class GatewayApplication {public static void main(String[] args) {SpringApplication.run(GatewayApplication.class,args);}
}
4、配置路由规则
server:port: 8080
spring:application:name: gatewaycloud:nacos:server-addr: 192.168.88.132:8848gateway:routes:- id: item # 路由规则id,自定义,唯一uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务- Path=/items/**,/search/** # 这里是以请求路径作为判断规则- id: carturi: lb://cart-servicepredicates:- Path=/carts/**- id: useruri: lb://user-servicepredicates:- Path=/users/**,/addresses/**- id: tradeuri: lb://trade-servicepredicates:- Path=/orders/**- id: payuri: lb://pay-servicepredicates:- Path=/pay-orders/**
2.2.2、路由属性
网关路由对应的Java类型是RouteDefinition,其中常见的属性有:
- id:路由唯一标示
- uri:路由目标地址
- predicates:路由断言,判断请求是否符合当前路由
- filters:路由过滤器,对请求或响应做特殊处理
Spring提供了12种基本的RoutePredicateFactory实现:
路由过滤器
名称 | 说明 | 示例 |
---|---|---|
After | 是某个时间点后的请求 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Before | 是某个时间点之前的请求 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between | 是某两个时间点之前的请求 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
Cookie | 请求必须包含某些cookie | - Cookie=chocolate, ch.p |
Header | 请求必须包含某些header | - Header=X-Request-Id, \d+ |
Host | 请求必须是访问某个host(域名) | - Host=.somehost.org,.anotherhost.org |
Method | 请求方式必须是指定方式 | - Method=GET,POST |
Path | 请求路径必须符合指定规则 | - Path=/red/{segment},/blue/** |
Query | 请求参数必须包含指定参数 | - Query=name, Jack或者- Query=name |
RemoteAddr | 请求者的ip必须是指定范围 | - RemoteAddr=192.168.1.1/24 |
weight | 权重处理 |
2.3、网关登录校验
网关请求处理流程
2.3.1、自定义过滤器
网关过滤器有两种,分别是:
- GatewayFilter:路由过滤器,作用于任意指定的路由;默认不生效,要配置到路由后生效
- GlobalFilter:全局过滤器,作用范围是所有路由;声明后自动生效
两种过滤器的过滤方法签名完全一致
GlobalFilter
自定义GlobalFilter比较简单,直接实现GlobalFilter接口即可
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// TODO 模拟登录校验逻辑ServerHttpRequest request = (ServerHttpRequest) exchange.getRequest();HttpHeaders headers = request.getHeaders();System.out.println("headers =" + headers);//放行return chain.filter(exchange);}@Overridepublic int getOrder() {return 0;}
}
2.3.2、实现登录校验
需求:在网关中基于过滤器实现登录校验功能
package com.hmall.gateway.filters;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();@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1.获取RequestServerHttpRequest request = exchange.getRequest();// 2.判断是否不需要拦截if(isExclude(request.getPath().toString())){// 无需拦截,直接放行return chain.filter(exchange);}// 3.获取请求头中的tokenString token = null;List<String> headers = request.getHeaders().get("authorization");if (!CollUtils.isEmpty(headers)) {token = headers.get(0);}// 4.校验并解析tokenLong 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;}@Overridepublic int getOrder() {return 0;}
}
2.3.3、网关传递用户
需求:修改gateway模块中的登录校验拦截器,在校验成功后保存到下游请求头中。
提示:要修改转发到微服务的请求,需要用到ServerWebExchange类提供的API,示例如下:
exchange.mutate() //mutate就是对下游请求做更改.request(builder->builder.header("user-info",userInfo)).build();
需求:由于每个微服务都可能有获取登录用户的需求,因此我们直接在hm-common模块定义拦截器,这样微服务只需要引入依赖即可生效,无需重复编写
首先,修改登录校验拦截器的处理逻辑,保存用户信息到请求头中:
// TODO 5.如果有效,传递用户信息
String userInfo = userId.toString();
ServerWebExchange ex = exchange.mutate().request(builder -> builder.header("user-info",userInfo)).build();
UserInfoInterceptor
public class UserInfoInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1、获取登录用户信息String userInfo = request.getHeader("user-info");//2、判断是否获取了用户,如果有,存入ThreadLocalif(StrUtil.isNotBlank(userInfo)){UserContext.setUser(Long.valueOf(userInfo));}//3、放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//清理用户UserContext.removeUser();}
}
MvcConfig
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {@Overridepublic 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.MvcConfig
之前我们无法获取登录用户,所以把购物车服务的登录用户写死了,现在需要恢复到原来的样子。
找到cart-service
模块的com.hmall.cart.service.impl.CartServiceImpl
:
@Overridepublic List<CartVO> queryMyCarts() {// 1.查询我的购物车列表List<Cart> carts = lambdaQuery().eq(Cart::getUserId, UserContext.getUser()).list();if (CollUtils.isEmpty(carts)) {return CollUtils.emptyList();}// 2.转换VOList<CartVO> vos = BeanUtils.copyList(carts, CartVO.class);// 3.处理VO中的商品信息handleCartItems(vos);// 4.返回return vos;}
2.3.4、OpenFeign传递用户
微服务项目中的很多业务要多个微服务共同合作完成,而这个过程也需要传递登录用户信息,例如:
OPenFeign中提供了一个拦截器接口,所有有OPenFeign发起的请求都会先调用拦截器处理请求
public class DefaultFeignConfig {@Beanpublic Logger.Level feignLogLevel(){return Logger.Level.FULL;}@Beanpublic RequestInterceptor useInfoRequestInterceptor(){return new RequestInterceptor() {@Overridepublic void apply(RequestTemplate requestTemplate) {Long userId = UserContext.getUser();if (userId!=null){requestTemplate.header("user-info",userId.toString());}}};}}
2.4、配置管理
- 微服务重复配置过多,维护成本高
- 业务配置经常变动,每次都要重启服务
- 网关路由配置写死,如果变更要重启网关
2.4.1、配置共享
添加一些共享配置到Nacos中,包括Jdbc,MybatisPlus、日志、Swagger、OPenFeign等配置
2.4.2、拉取共享配置
基于NacosConfig拉取共享配置代替微服务的本地配置
1、引入依赖
<!--nacos配置管理--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency><!--读取bootstrap文件--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bootstrap</artifactId></dependency>
2、新建bootstrap.yaml
spring:application:name: cart-service # 服务名称profiles:active: devcloud: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 # 共享日志配置
2.4.3、配置热更新
配置热更新:当修改配置文件中的配置时,微服务无需重启即可使配置生效
前提条件:
1、nacos中要有一个与微服务名有关的配置文件
2、微服务中要以特定方式读取需要热更新的配置属性(推荐第一种)
案例:实现购物车添加商品上限的配置热部署
需求:购物车的限定数量目前是写死在业务中的,将其改为读取配置文件属性,并将属性交给Nacos管理,实现热更新
@Data
@Component
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {private Integer maxItems;
}
CartServiceImpl
private CartProperties cartProperties;private void checkCartsFull(Long userId) {int count = Math.toIntExact(lambdaQuery().eq(Cart::getUserId, userId).count());if (count >= cartProperties.getMaxItems()) {throw new BizIllegalException(StrUtil.format("用户购物车课程不能超过{}", cartProperties.getMaxItems()));}}
测试:购物车中只能添加一个商品
2.4.4、动态路由
要实现动态路由首先要将路由配置保存到Nacos,当Nacos中的路由配置变更时,推送最新配置到网关,实时更新网关中的路由信息
我们要完成两件事情:
1、监听Nacos配置变更的消息
在Nacos管网中给出了手动监听Nacos配置变更的SDK:Java SDK (nacos.io)
private final NacosConfigManager nacosConfigManager;@PostConstructpublic void initRouteConfigListener() throws NacosException {//1.项目启动时,先拉取一次配置,并且添加配置监听器String configInfo = nacosConfigManager.getConfigService().getConfigAndSignListener(dataId, group, 5000, new Listener() {@Overridepublic Executor getExecutor() {return null;}@Overridepublic void receiveConfigInfo(String s) {//2.监听到配置变更,需要去更新路由表}});//3.第一次读取到配置,也需要更新到路由表updateConfigInfo(configInfo);}
2、当配置变更时,将最新的路由信息更新到网关路由表
监听到路由信息后,可以利用RouteDefinitionWriter来更新路由表
public interface RouteDefinitionWriter{//更新理由到路由表,如果路由id重复,则会覆盖旧的路由Mono<Void> save(Mono<RouteDefinition> route);//根据路由id删除某个路由Mono<void> delete(Mono<String> routeId);
}
路由配置语法
为了方便解析从Nacos读取到底路由配置,推荐使用json格式的路由配置,模块如下:
public void updateConfigInfo(String configInfo){log.debug("监听到路由配置信息"+configInfo);//1.解析配置信息,转为RouteDefinitionList<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);//2.更新前先删除旧的路由表for (String routeId : routeIds) {writer.delete(Mono.just(routeId)).subscribe();}routeIds.clear();//3.判断是否有新的路由要更新if (CollUtils.isEmpty(routeDefinitions)){//无新路由配置,直接结束return;}//4.更新路由routeDefinitions.forEach(routeDefinition -> {//更新路由writer.save(Mono.just(routeDefinition)).subscribe();//记录路由id,方便将来删除routeIds.add(routeDefinition.getId());});
}
在Nacos中新增配置
[{"id": "item","predicates": [{"name": "Path","args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}}],"filters": [],"uri": "lb://item-service"},{"id": "cart","predicates": [{"name": "Path","args": {"_genkey_0":"/carts/**"}}],"filters": [],"uri": "lb://cart-service"},{"id": "user","predicates": [{"name": "Path","args": {"_genkey_0":"/users/**", "_genkey_1":"/addresses/**"}}],"filters": [],"uri": "lb://user-service"},{"id": "trade","predicates": [{"name": "Path","args": {"_genkey_0":"/orders/**"}}],"filters": [],"uri": "lb://trade-service"},{"id": "pay","predicates": [{"name": "Path","args": {"_genkey_0":"/pay-orders/**"}}],"filters": [],"uri": "lb://pay-service"}
]
路由表的更新有一定的延迟
3、服务保护和分布式事务
3.1、雪崩问题
微服务调用链路中的某个服务故障,引起整个链路中所有微服务不可用,这就是雪崩
雪崩问题产生的原因是什么?
- 微服务互相调用,服务提供者出现故障或阻塞
- 服务调用者没有做好异常处理,导致自身故障
- 调用链中的所有服务级联失败,导致整个集群故障
解决问题的思路?
- 尽量避免服务出现故障或阻塞
- 保证代码的健壮性
- 保证网络畅通
- 能应对较高的并发需求
3.2、解决方案
3.2.1、请求限流
限制访问微服务的请求的并发量,避免服务因流量激增出现故障
3.2.2、线程隔离
线程隔离:也叫做舱壁模式,模拟船舱隔板的防水原理。通过限制每个业务能使用的线程数量而将故障业务隔离,避免故障扩展。
3.2.3、服务断熔
服务断熔:由断路器统计请求的异常比例或慢调用比例,如果超出阈值则会熔断该业务,则拦截该接口的请求
熔断期间,所有的请求快速失败,全都做fallback逻辑
3.3.4、服务保护技术
** ** | Sentinel | Hystrix |
---|---|---|
线程隔离 | 信号量隔离 | 线程池隔离/信号量隔离 |
熔断策略 | 基于慢调用比例或异常比例 | 基于异常比率 |
限流 | 基于 QPS,支持流量整形 | 有限的支持 |
Fallback | 支持 | 支持 |
控制台 | 开箱即用,可配置规则、查看秒级监控、机器发现等 | 不完善 |
配置方式 | 基于控制台,重启后失效 | 基于注解或配置文件,永久生效 |
3.3、Sentinel
3.3.1、初识Sentinel
Sentinel是阿里巴巴开源的一款微服务流量控制组件
官网:https://b11et3un53m.feishu.cn/wiki/QfVrw3sZvihmnPkmALYcUHIDnff#YRqVd7bn8odK9mx5F1tccqcrn2l
使用步骤:
1、下载jar包
2、运行
java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
在浏览器中输入localhost:8090
3.3.2、微服务整合
我们子啊cart-service模块中整合sentinel,连接sentinel-dashboard控制台,步骤如下:
1、引入sentinel依赖
<!--sentinel-->
<dependency><groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
2、配置控制台
修改application.yaml文件,添加下面内容:
spring:cloud: sentinel:transport:dashboard: localhost:8090
重启服务,建立与Sentinel的连接,在黑马商城中访问购物车的相关业务
3.3.3、簇点链路
簇点链路,就是单机调用链路。是一次请求进入服务后经过的每一个被Sentinel监控的资源链。默认Sentinel会监控SpringMVC的每一个EndPoint(http接口)。限流、熔断等都是针对簇点链路中的资源设置的。而资源名默认就是接口的请求路径:
RestFul风格的API请求一般都相同,这会导致簇点资源名称重复。因此我们要修改配置,把请求方式+请求路径作为簇点资源名称:
spring:cloud:sentinel:transport:dashboard: localhost:8090 #Sentinel的控制台地址http-method-specify: true #开启请求方式前缀
3.3.4、请求限流
在簇点链路后面点击流控按钮,即可对其做限流配置
Cloud.assets%5Cimage-20240808211321541.png&pos_id=img-2cbB1qiD-1723212019655)
在Jmeter中进行测试
如图,出现429代表实现限流
3.3.5、线程隔离
当商品服务出现阻塞或故障时,调用商品服务的购物车服务可能因此而被拖慢,甚至资源耗尽。所有必须限制购物车服务中查询商品这个业务的可用线程数,实现线程隔离
在Sentinel控制台中,会出现Feign接口的簇点资源,点击后面的流控按钮,即可配置线程隔离:
在ItemController中模拟业务延迟
@ApiOperation("根据id批量查询商品")@GetMappingpublic List<ItemDTO> queryItemByIds(@RequestParam("ids") List<Long> ids){//模拟业务延迟ThreadUtil.sleep(500);return itemService.queryItemByIds(ids);}
限制购物车模块的tomcat线程数
server:port: 8082tomcat:threads:max: 25accept-count: 25max-connections: 100
可以看出大部分都异常了
3.3.6、Fallback
1、将FeignClient作为Sentinel的簇点资源:
feign:sentinel:enabled: true
2、FeignClient的FallBack有两种配置方法:
- 方式一:FallbackClass,无法对远程调用的异常做处理
- 方式二:FallbackFactory,可以对远程调用的异常做处理,通常都会选这种
@Slf4j
public class ItemClientFallbackFactory implements FallbackFactory<ItemClient> {@Overridepublic ItemClient create(Throwable cause) {return new ItemClient() {@Overridepublic List<ItemDTO> queryItemByIds(Collection<Long> ids) {log.error("查询商品失败",cause);return CollUtils.emptyList();}@Overridepublic void deductStock(List<OrderDetailDTO> items) {log.error("扣减商品库存失败",cause);throw new RuntimeException(cause);}};}
}
DefaultFeignConfig
@Beanpublic ItemClientFallbackFactory itemClientFallbackFactory(){return new ItemClientFallbackFactory();}
ItemClient
@FeignClient(value = "item-service",fallbackFactory = ItemClientFallbackFactory.class)
public interface ItemClient {}
3.3.7、服务熔断
熔断是解决雪崩问题的主要手段。思路是由断路器统计服务调用异常的比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求,而当服务恢复时,断路器会放行访问该服务的请求
3.4、分布式事务
在分布式系统中,如果一个业务需要多个服务合作完成,而且每一个服务都有事务,多个事务必须同时成功或失败,这样的事务就是分布式事务。其中的每个服务就是一个分支事务。整个业务成为全局事务
下单业务,前端请求首先进入订单服务,创建订单并写入数据库。然后订单服务调用购物车服务和库存服务:
- 购物车服务负责清理购物车信息
- 库存服务负责扣减商品库存
3.4.1、初识Seata
Seata是2019年1月蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。致力于提供高性能和简单易用的分布式事务服务,为用户打造一站式的分布式解决方案
分布式事务解决思路:
Seata架构
Seata事务管理中有三个重要的角色:
- TC(Transaction Cooridinator)-事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚
- TM(Tansaction Manager)-事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务
- RM(Resource Manager)-资源管理器:管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态
3.4.2、部署TC服务
1、准备数据库表
导入数据库表seata
2、准备配置文件
导入seata目录到虚拟机
将nacos连接到网络
docker network connect hm-net nacos
3、Docker部署
docker run --name seata \
-p 8099:8099 \
-p 7099:7099 \
-e SEATA_IP=192.168.88.130 \
-v ./seata:/seata-server/resources \
--privileged=true \
--network hm-net \
-d \
seataio/seata-server:1.5.2
在浏览器中输入你的端口号:7099进入seata
3.4.3、微服务集成Seata
1、首先,要在项目中引入Seata依赖:
<!--统一配置管理--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency><!--读取bootstrap文件--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bootstrap</artifactId></dependency><!--seata--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId></dependency>
2、然后,在application.yml中添加配置,让微服务找到TC服务地址
seata:registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址type: nacos # 注册中心类型 nacosnacos:server-addr: 192.168.88.130:8848 # nacos地址namespace: "" # namespace,默认为空group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUPapplication: seata-server # seata服务名称username: nacospassword: nacostx-service-group: hmall # 事务组名称service:vgroup-mapping: # 事务组与tc集群的映射关系hmall: "default"
如果jdk是11以上版本要在启动类上修改
运行成功:
3.4.4、XA模式
XA模范是X/Open组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA规范描述了全局的TM与局部的RM之间的接口,几乎所有主流的关系型数据库都对XA规范提供了支持。Seata的XA模式如下:
一阶段工作:
- RM注册分支事务到TC
- RM执行分支业务sql但不提交
- RM报告执行状态到TC
二阶段工作:
- TC检测各分支事务执行状态
- a、如果都成功,通知所有RM提交事务
- b、如果有失败,通知所有RM回滚事务
- RM接收TC指令,提交或回滚事务
XA模式的有优点是什么?
- 事务的强一致性,满足ACID原则
- 常用数据库都支持,显示简单,并且没有代码侵入
XA模式的缺点是什么?
- 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
- 依赖关系型数据库实现事务
实现XA模式
Seata的starter已经完成了XA模式的自动配置,实现非常简单,步骤如下:
1、修改application.yml文件(每个参与事务的微服务),开启XA模式:
seata:data-source-proxy-mode: XA
2、给发起全局事务的入口方法添加@GlobalTransaction注解,本例中是OrderServiceImpl中的create方法:
3、重启服务并测试
3.4.5、AT模式
Seata主推的是AT模式,AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模式中组员锁定周期过长的缺陷
阶段一RM的工作:
- 注册分支事务
- 记录undo-log(数据快照)
- 执行业务sql并提交
- 报告事务状态
阶段二提交时RM的工作:
- 删除undo-log即可
阶段二回滚时RM的工作:
- 根据undo-log恢复数据到更新前
简述AT模式与XA模式最大的区别是什么?
- XA模式一阶段不提交事物,锁定资源;AT模式一阶段直接提交,不锁定资源。
- XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚
- XA模式强一致;AT模式最终
实现AT模式:
首先,添加资料汇总的Seata-at.sql到微服务对应的数据库中:
然后,修改application.yml文件,将事务模式修改为AT模式:
seata:data-source-proxy-mode: AT
913253" style=“zoom:50%;” />
如果jdk是11以上版本要在启动类上修改
[外链图片转存中…(img-QC5g9MwU-1723212019658)]
运行成功:
[外链图片转存中…(img-NEsbuGCP-1723212019658)]
3.4.4、XA模式
XA模范是X/Open组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA规范描述了全局的TM与局部的RM之间的接口,几乎所有主流的关系型数据库都对XA规范提供了支持。Seata的XA模式如下:
一阶段工作:
- RM注册分支事务到TC
- RM执行分支业务sql但不提交
- RM报告执行状态到TC
二阶段工作:
- TC检测各分支事务执行状态
- a、如果都成功,通知所有RM提交事务
- b、如果有失败,通知所有RM回滚事务
- RM接收TC指令,提交或回滚事务
[外链图片转存中…(img-R3xeQSL1-1723212019658)]
XA模式的有优点是什么?
- 事务的强一致性,满足ACID原则
- 常用数据库都支持,显示简单,并且没有代码侵入
XA模式的缺点是什么?
- 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
- 依赖关系型数据库实现事务
实现XA模式
Seata的starter已经完成了XA模式的自动配置,实现非常简单,步骤如下:
1、修改application.yml文件(每个参与事务的微服务),开启XA模式:
seata:data-source-proxy-mode: XA
2、给发起全局事务的入口方法添加@GlobalTransaction注解,本例中是OrderServiceImpl中的create方法:
3、重启服务并测试
3.4.5、AT模式
Seata主推的是AT模式,AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模式中组员锁定周期过长的缺陷
阶段一RM的工作:
- 注册分支事务
- 记录undo-log(数据快照)
- 执行业务sql并提交
- 报告事务状态
阶段二提交时RM的工作:
- 删除undo-log即可
阶段二回滚时RM的工作:
- 根据undo-log恢复数据到更新前
[外链图片转存中…(img-y05XX17y-1723212019659)]
简述AT模式与XA模式最大的区别是什么?
- XA模式一阶段不提交事物,锁定资源;AT模式一阶段直接提交,不锁定资源。
- XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚
- XA模式强一致;AT模式最终
实现AT模式:
首先,添加资料汇总的Seata-at.sql到微服务对应的数据库中:
然后,修改application.yml文件,将事务模式修改为AT模式:
seata:data-source-proxy-mode: AT