一下内容为本人在听黑马程序员的课程时整理的
⎛⎝≥⏝⏝≤⎛⎝ ⎛⎝≥⏝⏝≤⎛⎝ ⎛⎝≥⏝⏝≤⎛⎝ ⎛⎝≥⏝⏝≤⎛⎝
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.获取商品id
Set<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的map
Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
// 4.写入vo
for (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{
@Bean
public 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: gateway
cloud:
nacos:
server-addr: 192.168.88.132:8848
gateway:
routes:
- id: item # 路由规则id,自定义,唯一
uri: lb://item-service # 路由的目标服务,lb代表负载均衡,会从注册中心拉取服务列表
predicates: # 路由断言,判断当前请求是否符合当前规则,符合则路由到目标服务
- Path=/items/**,/search/** # 这里是以请求路径作为判断规则
- id: cart
uri: lb://cart-service
predicates:
- Path=/carts/**
- id: user
uri: lb://user-service
predicates:
- Path=/users/**,/addresses/**
- id: trade
uri: lb://trade-service
predicates:
- Path=/orders/**
- id: pay
uri: lb://pay-service
predicates:
- 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 {
@Override
public 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);
}
@Override
public 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();
@Override
public Mono<Void> 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;
List<String> 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;
}
}
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 {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//1、获取登录用户信息
String userInfo = request.getHeader("user-info");
//2、判断是否获取了用户,如果有,存入ThreadLocal
if(StrUtil.isNotBlank(userInfo)){
UserContext.setUser(Long.valueOf(userInfo));
}
//3、放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//清理用户
UserContext.removeUser();
}
}
MvcConfig
@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.MvcConfig
之前我们无法获取登录用户,所以把购物车服务的登录用户写死了,现在需要恢复到原来的样子。
找到cart-service
模块的com.hmall.cart.service.impl.CartServiceImpl
:
@Override
public List<CartVO> queryMyCarts() {
// 1.查询我的购物车列表
List<Cart> carts = lambdaQuery().eq(Cart::getUserId, UserContext.getUser()).list();
if (CollUtils.isEmpty(carts)) {
return CollUtils.emptyList();
}
// 2.转换VO
List<CartVO> vos = BeanUtils.copyList(carts, CartVO.class);
// 3.处理VO中的商品信息
handleCartItems(vos);
// 4.返回
return vos;
}
2.3.4、OpenFeign传递用户
微服务项目中的很多业务要多个微服务共同合作完成,而这个过程也需要传递登录用户信息,例如:
OPenFeign中提供了一个拦截器接口,所有有OPenFeign发起的请求都会先调用拦截器处理请求
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.FULL;
}
@Bean
public RequestInterceptor useInfoRequestInterceptor(){
return new RequestInterceptor() {
@Override
public 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: 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 # 共享日志配置
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;
@PostConstruct
public void initRouteConfigListener() throws NacosException {
//1.项目启动时,先拉取一次配置,并且添加配置监听器
String configInfo = nacosConfigManager.getConfigService()
.getConfigAndSignListener(dataId, group, 5000, new Listener() {
@Override
public Executor getExecutor() {
return null;
}
@Override
public 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.解析配置信息,转为RouteDefinition
List<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批量查询商品")
@GetMapping
public List<ItemDTO> queryItemByIds(@RequestParam("ids") List<Long> ids){
//模拟业务延迟
ThreadUtil.sleep(500);
return itemService.queryItemByIds(ids);
}
限制购物车模块的tomcat线程数
server:
port: 8082
tomcat:
threads:
max: 25
accept-count: 25
max-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> {
@Override
public ItemClient create(Throwable cause) {
return new ItemClient() {
@Override
public List<ItemDTO> queryItemByIds(Collection<Long> ids) {
log.error("查询商品失败",cause);
return CollUtils.emptyList();
}
@Override
public void deductStock(List<OrderDetailDTO> items) {
log.error("扣减商品库存失败",cause);
throw new RuntimeException(cause);
}
};
}
}
DefaultFeignConfig
@Bean
public 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 # 注册中心类型 nacos
nacos:
server-addr: 192.168.88.130:8848 # nacos地址
namespace: "" # namespace,默认为空
group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
application: seata-server # seata服务名称
username: nacos
password: nacos
tx-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