当前位置: 首页 > news >正文

【SpringCloud】从入门到精通(下)

网关与路由

什么是网关?顾明思议,网关就是网络的关口。数据在网络间传输,从一个网络传输到另一网络时就需要经过网关来做数据的路由和转发以及数据安全的校验。
现在前端不能请求各个微服务地址,只能去请求网关

  • 网关可以做安全控制,也就是登录身份校验,校验通过才放行
  • 通过认证后,网关再根据请求判断应该访问哪个微服务,将请求转发过去

在这里插入图片描述
在SpringCloud当中,提供了两种网关实现方案:

  • Netflix Zuul:早期实现,目前已经淘汰
  • SpringCloudGateway:基于Spring的WebFlux技术,完全支持响应式编程,吞吐能力更强

因此,我们选择性能更好的SpringCloudGateway来作为我们微服务的网关

快速入门

由于网关本身也是一个独立的微服务,因此也需要创建一个模块开发功能。大概步骤如下:

  • 创建网关微服务
  • 引入SpringCloudGateway、NacosDiscovery依赖
  • 编写启动类
  • 配置网关路由
创建项目

名为hm-gatery父工程为hmall
在这里插入图片描述

引入依赖
<?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>
编写启动类
package com.hmall.gateway;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class GatewayApplication {public static void main(String[] args) {SpringApplication.run(GatewayApplication.class, args);}
}
配置路由
server:port: 8080
spring:application:name: gatewaycloud:nacos:server-addr: 192.168.150.101: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/**

四个属性含义如下:

  • id:路由的唯一标示
  • predicates:路由断言,其实就是匹配条件
  • filters:路由过滤条件,后面讲
  • uri:路由目标地址,lb://代表负载均衡,从注册中心获取目标微服务的实例列表,并且负载均衡选择一个访问。

这里我们重点关注路由断言

名称说明示例
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权重处理

网关的登录与校验

单体架构时我们只需要完成一次用户登录、身份校验,就可以在所有业务中获取到用户信息。而微服务拆分后,每个微服务都独立部署,不再共享数据。也就意味着每个微服务都需要做登录校验,这显然不可取。

鉴权思路分析

我们的登录是基于JWT来实现的,校验JWT的算法复杂,而且需要用到秘钥。如果每个微服务都去做登录校验,这就存在着两大问题:

  • 每个微服务都需要知道JWT的秘钥,不安全
  • 每个微服务重复编写登录校验代码、权限校验代码,麻烦

那么该怎么办呢,其实我们所有的请求都会经过网关,如果在网关当中对用户身份进行校验,那么就可以避免这些问题
在这里插入图片描述

网关过滤器

登录校验必须在请求转发到微服务之前做,否则就失去了意义。而网关的请求转发是Gateway内部代码实现的,要想在请求转发之前做登录校验,就必须了解Gateway内部工作的基本原理。
在这里插入图片描述

  1. 客户端请求进入网关后由HandlerMapping对请求做判断,找到与当前请求匹配的路由规则(Route),然后将请求交给WebHandler去处理。
  2. WebHandler则会加载当前路由下需要执行的过滤器链(Filter chain),然后按照顺序逐一执行过滤器(后面称为Filter)。
  3. 图中Filter被虚线分为左右两部分,是因为Filter内部的逻辑分为prepost两部分,分别会在请求路由到微服务之前和之后被执行。
  4. 只有所有Filterpre逻辑都依次顺序执行通过后,请求才会被路由到微服务。
  5. 微服务返回结果后,再倒序执行Filterpost逻辑。
  6. 最终把响应结果返回。

如图所示,当中有一个名为NettyRoutingFilter的过滤器,他是过滤器链中的最后一个(承担着将请求分发给微服务的作用),那么试想一下,如果我们能够自己定义一个过滤器,在NettyRoutingFilter之前,并且在pre部分,那么是不是就可以对用户身份进行校验。

网关过滤器链中的过滤器有两种:

  • GatewayFilter:路由过滤器,作用范围比较灵活,可以是任意指定的路由Route.
  • GlobalFilter:全局过滤器,作用范围是所有路由,不可配置

其实GatewayFilter和GlobalFilter这两种过滤器的方法签名完全一致:

/*** 处理请求并将其传递给下一个过滤器* @param exchange 当前请求的上下文,其中包含request、response等各种数据* @param chain 过滤器链,基于它向下传递请求* @return 根据返回值标记当前请求是否被完成或拦截,chain.filter(exchange)就放行了。*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);

Gateway内置的GatewayFilter过滤器使用起来非常简单,无需编码,只要在yaml文件中简单配置即可。而且其作用范围也很灵活,配置在哪个Route下,就作用于哪个Route.

spring:cloud:gateway:routes:- id: test_routeuri: lb://test-servicepredicates:-Path=/test/**filters:- AddRequestHeader=key, value # 逗号之前是请求头的key,逗号之后是value

想让过滤器作用与所有路由

spring:cloud:gateway:default-filters: # default-filters下的过滤器可以作用于所有路由- AddRequestHeader=key, valueroutes:- id: test_routeuri: lb://test-servicepredicates:-Path=/test/**

自定义过滤器

自定义GatewayFilter

自定义GatewayFilter不是直接实现GatewayFilter,而是实现AbstractGatewayFilterFactory。最简单的方式是这样的:

@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {@Overridepublic GatewayFilter apply(Object config) {return new GatewayFilter() {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 获取请求ServerHttpRequest request = exchange.getRequest();// 编写过滤器逻辑System.out.println("过滤器执行了");// 放行return chain.filter(exchange);}};}
}

注意:该类的名称一定要以GatewayFilterFactory为后缀!

然后在yaml配置中这样使用:

spring:cloud:gateway:default-filters:- PrintAny # 此处直接以自定义的GatewayFilterFactory类名称前缀类声明过滤器
自定义GlobalFilter
@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 编写过滤器逻辑System.out.println("未登录,无法访问");// 放行// return chain.filter(exchange);// 拦截ServerHttpResponse response = exchange.getResponse();response.setRawStatusCode(401);return response.setComplete();}@Overridepublic int getOrder() {// 过滤器执行顺序,值越小,优先级越高return 0;}
}

登录校验

接下来,我们就利用自定义GlobalFilter来完成登录校验。

@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;}
}

微服务获取用户信息

现在,网关已经可以完成登录校验并获取登录用户身份信息。但是当网关将请求转发到微服务时,微服务又该如何获取用户身份呢?
服务又该如何获取用户身份呢?
由于网关发送请求到微服务依然采用的是Http请求,因此我们可以将用户信息以请求头的方式传递到下游微服务。然后微服务可以从请求头中获取登录用户信息。考虑到微服务内部可能很多地方都需要用到登录用户信息,因此我们可以利用SpringMVC的拦截器来实现登录用户信息获取,并存入ThreadLocal,方便后续使用。
在这里插入图片描述

保存用户信息到请求头

还在上面定义的那个拦截器当中,我们将第五步改造一下
在这里插入图片描述

拦截器获取用户

看我们整个项目,每个微服务模块都用到了hm-common,因此我们可以把MVC的拦截器定义在common当中,这样就不用在每个微服务模块定义拦截器了,并且在common当中定义一个存储用户信息的ThreadLocal工具。

public class UserInfoInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取请求头中的用户信息String userInfo = request.getHeader("user-info");// 2.判断是否为空if (StrUtil.isNotBlank(userInfo)) {// 不为空,保存到ThreadLocalUserContext.setUser(Long.valueOf(userInfo));}// 3.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用户UserContext.removeUser();}
}

MVC注册拦截器,在这里我们加上这个条件@ConditionalOnClass(DispatcherServlet.class)是因为网关有自己的一套拦截流程和MVC那一套不一样,网关当中也引入了common模块,如果把MVCConfig也变为网关当中一个Bean,势必会报错,因此我们加上这个条件,因为网关当中没有引入MVC的依赖,所以就不会注册这个Bean

@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

OpenFeign传递用户

我们想象,我们是不是有其他微服务的业务会调用其他微服务,刚好这个被调用的微服务要获取到用户的id,那么我们该怎么传递呢。
这里要借助Feign中提供的一个拦截器接口:feign.RequestInterceptor

public interface RequestInterceptor {/*** Called for every request. * Add data using methods on the supplied {@link RequestTemplate}.*/void apply(RequestTemplate template);
}
@Bean
public RequestInterceptor userInfoRequestInterceptor(){return new RequestInterceptor() {@Overridepublic void apply(RequestTemplate template) {// 获取登录用户Long userId = UserContext.getUser();if(userId == null) {// 如果为空则直接跳过return;}// 如果不为空则放入请求头中,传递给下游微服务template.header("user-info", userId.toString());}};
}

配置管理

我们的微服务项目还有以下几个问题需要解决

  • 网关路由在配置文件中写死了,如果变更必须重启微服务
  • 某些业务配置在配置文件中写死了,每次修改都要重启服务
  • 每个微服务都有很多重复的配置,维护成本高

添加共享配置

我们可以吧每个微服务模块中的公共模块都抽取出来,放到nacos当中
在这里插入图片描述

拉取共享配置

接下来,我们要在微服务拉取共享配置。将拉取到的共享配置与本地的application.yaml配置合并,完成项目上下文的初始化。
不过,需要注意的是,读取Nacos配置是SpringCloud上下文(ApplicationContext)初始化时处理的,发生在项目的引导阶段。然后才会初始化SpringBoot上下文,去读取application.yaml。
也就是说引导阶段,application.yaml文件尚未读取,根本不知道nacos 地址,该如何去加载nacos中的配置文件呢?流程如下图
在这里插入图片描述

也就是说,我们如果把nacos的配置放在application.yaml文件当中,SpringCloud根本就不知道从哪里拉取配置,导致项目启动失败,这时候我们就需要用到bootstrap.yaml

引入依赖
  <!--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>
新建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 # 共享日志配置

配置热更新

有很多的业务相关参数,将来可能会根据实际情况临时调整。例如购物车业务,购物车数量有一个上限,默认是10,对应代码如下:
在这里插入图片描述
但是现在有个问题,如果有一天我们想更改这个数值,那么就只能去重启项目然后才能更改这个数值,这显然非常麻烦
这就要用到Nacos的配置热更新能力了,分为两步:

  • 在Nacos中添加配置
  • 在微服务读取配置
添加配置到nacos

在这里插入图片描述

我们这里要注意dataId的格式:[服务名]-[spring.active.profile].[后缀名]

文件名称由三部分组成:

  • 服务名:我们是购物车服务,所以是cart-service
  • spring.active.profile:就是spring boot中的spring.active.profile,可以省略,则所有profile共享该配置
  • 后缀名:例如yaml

这里我们直接使用cart-service.yaml这个名称,则不管是dev还是local环境都可以共享该配置。
配置内容如下:

hm:cart:maxAmount: 1 # 购物车商品数量上限

最后提交配置

配置热更新

接着,我们在微服务中读取配置,实现配置热更新。
cart-service中新建一个属性读取类:

package com.hmall.cart.config;import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;@Data
@Component
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {private Integer maxAmount;
}

接着在代码中更改,然后我们试着在nacos中更改这个数值,测试后就会发现他会随着我们的更改而改变
在这里插入图片描述

动态路由

网关的路由配置全部是在项目启动时由org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator在项目启动的时候加载,并且一经加载就会缓存到内存中的路由表内(一个Map),不会改变。也不会监听路由变更,所以,我们无法利用上节课学习的配置热更新来实现路由更新。

那么我们该怎么更改路由呢

监听Nacos配置变更

如果希望 Nacos 推送配置变更,可以使用 Nacos 动态监听配置接口来实现。

public void addListener(String dataId, String group, Listener listener)
  • dataId(String) 配置 ID,保证全局唯一性,只允许英文字符和 4 种特殊字符(“.”、“:”、“-”、“_”)。不超过 256 字节。
  • group(String) 配置分组,一般是默认的DEFAULT_GROUP。
  • listener(Listener) 监听器,配置变更进入监听器的回调函数。

实例代码

String serverAddr = "{serverAddr}";
String dataId = "{dataId}";
String group = "{group}";
// 1.创建ConfigService,连接Nacos
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
ConfigService configService = NacosFactory.createConfigService(properties);
// 2.读取配置
String content = configService.getConfig(dataId, group, 5000);
// 3.添加配置监听器
configService.addListener(dataId, group, new Listener() {@Overridepublic void receiveConfigInfo(String configInfo) {// 配置变更的通知处理System.out.println("recieve1:" + configInfo);}// 方法返回 null 时,Nacos 会使用默认的执行器来处理配置变更通知。默认执行器通常会在 Nacos 客户端的内部线程中执行 receiveConfigInfo 方法。@Overridepublic Executor getExecutor() {return null;}
});

这里核心的步骤有2步:

  • 创建ConfigService,目的是连接到Nacos
  • 添加配置监听器,编写配置变更的通知处理逻辑

由于我们采用了spring-cloud-starter-alibaba-nacos-config自动装配,因此ConfigService已经在com.alibaba.cloud.nacos.NacosConfigAutoConfiguration中自动创建好了:
在这里插入图片描述
NacosConfigManager中是负责管理Nacos的ConfigService的,具体代码如下:
在这里插入图片描述
因此,只要我们拿到NacosConfigManager就等于拿到了ConfigService,第一步就实现了。

第二步,编写监听器。虽然官方提供的SDK是ConfigService中的addListener,不过项目第一次启动时不仅仅需要添加监听器,也需要读取配置,因此建议使用的API是这个:

String getConfigAndSignListener(String dataId, // 配置文件idString group, // 配置组,走默认long timeoutMs, // 读取配置的超时时间Listener listener // 监听器
) throws NacosException;

这时候我们就可以在项目启动的时候先配置一次路由,再在配置更新的时候去更新路由

更新路由

更新路由要用到org.springframework.cloud.gateway.route.RouteDefinitionWriter这个接口:

package org.springframework.cloud.gateway.route;import reactor.core.publisher.Mono;/*** @author Spencer Gibb*/
public interface RouteDefinitionWriter {/*** 更新路由到路由表,如果路由id重复,则会覆盖旧的路由*/Mono<Void> save(Mono<RouteDefinition> route);/*** 根据路由id删除某个路由*/Mono<Void> delete(Mono<String> routeId);}
  • id:路由id
  • predicates:路由匹配规则
  • filters:路由过滤器
  • uri:路由目的地

将来我们保存到Nacos的配置也要符合这个对象结构,将来我们以JSON来保存,格式如下:

{"id": "item","predicates": [{"name": "Path","args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}}],"filters": [],"uri": "lb://item-service"
}

等同于下面

spring:cloud:gateway:routes:- id: itemuri: lb://item-servicepredicates:- Path=/items/**,/search/**
实现动态路由

引入一下依赖

<!--统一配置管理-->
<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>

配置bootstrap.yaml

spring:application:name: gatewaycloud:nacos:server-addr: 192.168.150.101config:file-extension: yamlshared-configs:- dataId: shared-log.yaml # 共享日志配置

在gatery中配置路由监听器

@Slf4j
@Component
@RequiredArgsConstructor
public class DynamicRouteLoader {private final RouteDefinitionWriter writer;private final NacosConfigManager nacosConfigManager;// 路由配置文件的id和分组private final String dataId = "gateway-routes.json";private final String group = "DEFAULT_GROUP";// 保存更新过的路由idprivate final Set<String> routeIds = new HashSet<>();@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 configInfo) {updateConfigInfo(configInfo);}});// 2.首次启动时,更新一次配置updateConfigInfo(configInfo);}private void updateConfigInfo(String configInfo) {log.debug("监听到路由配置变更,{}", configInfo);// 1.反序列化List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);// 2.更新前先清空旧路由// 2.1.清除旧路由for (String routeId : routeIds) {writer.delete(Mono.just(routeId)).subscribe();}routeIds.clear();// 2.2.判断是否有新的路由要更新if (CollUtils.isEmpty(routeDefinitions)) {// 无新路由配置,直接结束return;}// 3.更新路由routeDefinitions.forEach(routeDefinition -> {// 3.1.更新路由writer.save(Mono.just(routeDefinition)).subscribe();// 3.2.记录路由id,方便将来删除routeIds.add(routeDefinition.getId());});}
}

subscribe()方法的主要作用是订阅一个 Mono 或者 Flux 序列,进而触发序列的执行。在响应式编程里,Mono 或者 Flux 所定义的操作属于声明式的,只有在被订阅时才会真正执行

微服务保护

在微服务远程调用当中仍然有几个问题需要我们去解决

  • 首先是业务的健壮性问题。
    例如我们之前的查询购物车的接口,需要查询到商品的最新信息,然后与之前的价格作对比,比较价格,但是如果商品查询服务发生了故障呢,那么查询购物车的接口也会发生故障,但是这是我们不愿意看到的,我们希望的是购物车接口能够正常返回结果,即使没有最新的商品信息
  • 还有级联的失败问题
    还是查询购物车的业务,假如商品服务业务并发较高,占用过多Tomcat连接。可能会导致商品服务的所有接口响应时间增加,延迟变高,甚至是长时间阻塞直至查询失败。
    此时查询购物车业务需要查询并等待商品查询结果,从而导致查询购物车列表业务的响应时间也变长,甚至也阻塞直至无法访问。而此时如果查询购物车的请求较多,可能导致购物车服务的Tomcat连接占用较多,所有接口的响应时间都会增加,整个服务性能很差, 甚至不可用。
    在这里插入图片描述
    依次类推,整个微服务群中与购物车服务、商品服务等有调用关系的服务可能都会出现问题,最终导致整个集群不可用。
    在这里插入图片描述

还有跨微服务的事务问题,比如下单业务,经过一下过程

  • 商品服务:扣减库存
  • 订单服务:保存订单
  • 购物车服务:清理购物车

这些业务全部都是数据库的写操作,我们必须确保所有操作的同时成功或失败。但是这些操作在不同微服务,也就是不同的Tomcat,这样的情况如何确保事务特性呢?

服务保护方案

服务保护方案有以下三种

  • 请求限流
  • 线程隔离
  • 服务熔断
    请求限流,降低了并发上限;线程隔离,降低了可用资源数量;服务熔断,降低了服务的完整度,部分服务变的不可用或弱可用。因此这些方案都属于服务降级的方案。但通过这些方案,服务的健壮性得到了提升,
    接下来,我们就逐一了解这些方案的原理。
请求限流

我们想,我们服务故障出现的原因一部分是因为该服务的并发太高,导致服务资源耗尽,最终宕机。因此请求限流就能很好的解决这个问题,能够避免服务流量激增。
请求限流往往会有一个限流器,数量高低起伏的并发请求曲线,经过限流器就变的非常平稳。这就像是水电站的大坝,起到蓄水的作用,可以通过开关控制水流出的大小,让下游水流始终维持在一个平稳的量。
在这里插入图片描述

线程隔离

当一个业务接口响应时间长,而且并发高时,就可能耗尽服务器的线程资源,导致服务内的其它接口受到影响。所以我们必须把这种影响降低,或者缩减影响的范围。线程隔离正是解决这个问题的好办法。
为了避免某个接口故障或压力过大导致整个服务不可用,我们可以限定每个接口可以使用的资源范围,也就是将其“隔离”起来。
在这里插入图片描述

服务熔断

线程隔离虽然避免了雪崩问题,但故障服务(商品服务)依然会拖慢购物车服务(服务调用方)的接口响应速度。而且商品查询的故障依然会导致查询购物车功能出现故障,购物车业务也变的不可用了。

所以,我们要做两件事情:

  • 编写服务降级逻辑:就是服务调用失败后的处理逻辑,根据业务场景,可以抛出异常,也可以返回友好提示或默认数据。
  • 异常统计和熔断:统计服务提供方的异常比例,当比例过高表明该接口会影响到其它服务,应该拒绝调用该接口,而是直接走降级逻辑。

在这里插入图片描述

Sentinel

Sentinel是阿里巴巴开源的一款服务保护框架,目前已经加入SpringCloudAlibaba中。国内开源还带是我阿里巴巴哥。

官方网站:Sentinel官方网站

安装

Sentinel 的使用可以分为两个部分:

  • 核心库(Jar包):不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。在项目中引入依赖即可实现服务限流、隔离、熔断等功能。
  • 控制台(Dashboard):Dashboard 主要负责管理推送规则、监控、管理机器信息等。

(1)下载jar包

地址:下载地址

(2)运行
将jar包放在任意非中文、不包含特殊字符的目录下运行,运行时不要打开powershell,而是要在文件地址当中输入cmd然后运行

java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar

(3)访问
运行无错误之后,访问8090端口,可以看到控制台

微服务整合

导入依赖

<!--sentinel-->
<dependency><groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

设置配置

spring:cloud: sentinel:transport:dashboard: localhost:8090

请求限流

在簇点链路后面点击流控按钮,即可对其做限流配置:
在这里插入图片描述
QPS单机阈值也就是每秒最大请求数量
在这里插入图片描述

线程隔离

限流可以降低服务器压力,尽量减少因并发流量引起的服务故障的概率,但并不能完全避免服务故障。一旦某个服务出现故障,我们必须隔离对这个服务的调用,避免发生雪崩。

比如,查询购物车的时候需要查询商品,为了避免因商品服务出现故障导致购物车服务级联失败,我们可以把购物车业务中查询商品的部分隔离起来,限制可用的线程资源:
在这里插入图片描述
这样,即便商品服务出现故障,最多导致查询购物车业务故障,并且可用的线程资源也被限定在一定范围,不会导致整个购物车服务崩溃。

OpenFeign整合Sentinel

配置

feign:sentinel:enabled: true # 开启feign对sentinel的支持

然后重启服务,可以看到查询商品的FeignClient自动变成了一个簇点资源:
在这里插入图片描述

配置线程隔离

在这里插入图片描述
在这里插入图片描述
设置并发线程数

服务熔断

在上节课,我们利用线程隔离对查询购物车业务进行隔离,保护了购物车服务的其它接口。由于查询商品的功能耗时较高(我们模拟了500毫秒延时),再加上线程隔离限定了线程数为5,导致接口吞吐能力有限,最终QPS只有10左右。这就导致了几个问题:

  • 第一,超出的QPS上限的请求就只能抛出异常,从而导致购物车的查询失败。但从业务角度来说,即便没有查询到最新的商品信息,购物车也应该展示给用户,用户体验更好。也就是给查询失败设置一个降级处理逻辑。
  • 第二,由于查询商品的延迟较高(模拟的500ms),从而导致查询购物车的响应时间也变的很长。这样不仅拖慢了购物车服务,消耗了购物车服务的更多资源,而且用户体验也很差。对于商品服务这种不太健康的接口,我们应该直接停止调用,直接走降级逻辑,避免影响到当前服务。也就是将商品查询接口熔断。

也就是说虽然进行了线程隔离,但是当许多请求打过来的时候,还是会影响统一微服务的其他业务,并且还会影响用户的体验

编写降级逻辑

触发限流或熔断后的请求不一定要直接报错,也可以返回一些默认数据或者友好提示,用户体验会更好。
给FeignClient编写失败后的降级逻辑有两种方式:

  • 方式一:FallbackClass,无法对远程调用的异常做处理
  • 方式二:FallbackFactory,可以对远程调用的异常做处理,我们一般选择这种方式。
    这里我们使用第二种方法来实现降级处理

步骤一:在hm-api模块中给ItemClient定义降级处理类,实现FallbackFactory:

@Slf4j
public class ItemClientFallback implements FallbackFactory<ItemClient> {@Overridepublic ItemClient create(Throwable cause) {return new ItemClient() {@Overridepublic List<ItemDTO> queryItemByIds(Collection<Long> ids) {log.error("远程调用ItemClient#queryItemByIds方法出现异常,参数:{}", ids, cause);// 查询购物车允许失败,查询失败,返回空集合return CollUtils.emptyList();}@Overridepublic void deductStock(List<OrderDetailDTO> items) {// 库存扣减业务需要触发事务回滚,查询失败,抛出异常throw new BizIllegalException(cause);}};}
}

步骤二:在hm-api模块中的com.hmall.api.config.DefaultFeignConfig类中将ItemClientFallback注册为一个Bean:

@Bean
public ItemClientFallbackFactory itemClientFallbackFactory() {return new ItemClientFallbackFactory();
}

步骤三:在hm-api模块中的ItemClient接口中使用ItemClientFallbackFactory
在这里插入图片描述
重启后,再次测试,发现被限流的请求不再报错,走了降级逻辑。

服务熔断

查询商品的RT较高(模拟的500ms),从而导致查询购物车的RT也变的很长。这样不仅拖慢了购物车服务,消耗了购物车服务的更多资源,而且用户体验也很差。如果这个服务不是慢,而是挂了,那么每次请求来我们还要去请求他,而我们明明知道他已经挂了。
对于商品服务这种不太健康的接口,我们应该停止调用,直接走降级逻辑,避免影响到当前服务。也就是将商品查询接口熔断。当商品服务接口恢复正常后,再允许调用。这其实就是断路器的工作模式了。
在这里插入图片描述
状态机包括三个状态:

  • closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
  • open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态持续一段时间后会进入half-open状态
  • half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
    • 请求成功:则切换到closed状态
    • 请求失败:则切换到open状态

在这里插入图片描述
在这里插入图片描述
[图片]
这种是按照慢调用比例来做熔断,上述配置的含义是:

  • RT超过200毫秒的请求调用就是慢调用
  • 统计最近1000ms内的最少5次请求,如果慢调用比例不低于0.5,则触发熔断
  • 熔断持续时长20s

分布式事务

首先我们看看项目中的下单业务整体流程:
在这里插入图片描述
由于创建订单,清空购物车和扣减库存这三个动作在三个不同的微服务当中。因此下单业务就会跨多个数据库完成业务,而每个微服务都会执行自己本地的事务

  • 交易服务:下单事务
  • 购物车服务:清理购物车事务
  • 库存服务:扣减库存事务

整个业务中,各个本地事务是有关联的。因此每个微服务的本地事务,也可以称为分支事务。多个有关联的分支事务一起就组成了全局事务。我们必须保证整个全局事务同时成功或失败。
我们知道每一个分支事务就是传统的单体事务,都可以满足ACID特性,但全局事务跨越多个服务、多个数据库,是否还能满足呢?

ACID

  • 原子性(Atomicity):事务是一个不可分割的工作单位,事务中的所有操作要么全部成功执行,要么全部不执行。例如,在银行转账的事务中,从一个账户扣款和向另一个账户存款这两个操作必须同时成功或者同时失败,不能出现只完成其中一个操作的情况。
  • 一致性(Consistency):事务执行前后,数据库的完整性约束没有被破坏,数据库始终处于一致的状态。比如,在转账事务中,转账前两个账户的余额总和与转账后两个账户的余额总和应该是相等的,满足数据的一致性要求。
  • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不能被其他事务干扰,各个事务之间相互隔离。不同的隔离级别会对事务的并发执行产生不同的影响,例如读未提交、读已提交、可重复读和串行化等隔离级别,从低到高逐渐增强隔离性。
  • 持久性(Durability):事务一旦提交,它对数据库所做的修改就会永久保存下来,即使数据库系统出现故障也不会丢失。例如,在转账事务提交后,即使数据库服务器突然断电,重新启动后,转账的结果也应该是持久保存的,不会恢复到转账前的状态。

经过我的测试,不同微服务的事务合在一起并不会遵守ACID,归其原因就是参与事务的多个子业务在不同的微服务,跨越了不同的数据库。虽然每个单独的业务都能在本地遵循ACID,但是它们互相之间没有感知,不知道有人失败了,无法保证最终结果的统一,也就无法遵循ACID的事务特性了。

Seata

解决分布式事务的方案有很多,但实现起来都比较复杂,因此我们一般会使用开源的框架来解决分布式事务问题。在众多的开源分布式事务框架中,功能最完善、使用最多的就是阿里巴巴在2019年开源的Seata了。(又是我阿里巴巴)

Seata官网

分布式事务无法一致的原因就是不同微服务之间无法相互感知,你不知道我,我不知道你,彼此不知道彼此的状态,因此解决的思想也非常简单:
就是找一个统一的事务协调者,与多个分支事务通信,检测每个分支事务的执行状态,保证全局事务下的每一个分支事务同时成功或失败即可。大多数的分布式事务框架都是基于这个理论来实现的。

Seata也不例外,在Seata的事务管理中有三个重要的角色:

  • TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚。
  • TM (Transaction Manager) - 事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager) - 资源管理器:管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

在这里插入图片描述
其中,TM和RM可以理解为Seata的客户端部分,引入到参与事务的微服务依赖中即可。将来TM和RM就会协助微服务,实现本地分支事务与TC之间交互,实现事务的提交或回滚。

而TC服务则是事务协调中心,是一个独立的微服务,需要单独部署。

部署TC服务

准备表

seata.sql可以去黑马资料当中下载

准备配置文件

修改seata配置文件当中nacos地址和mysql地址

Docker部署
docker run --name seata \
-p 8099:8099 \
-p 7099:7099 \
-e SEATA_IP=192.168.150.101 \
-v ./seata:/seata-server/resources \
--privileged=true \
--network hm-net \
-d \
seataio/seata-server:1.5.2

微服务集成Seata

引入依赖
  <!--seata--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId></dependency>
配置
seata:registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址type: nacos # 注册中心类型 nacosnacos:server-addr: 192.168.150.101:8848 # nacos地址namespace: "" # namespace,默认为空group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUPapplication: seata-server # seata服务名称username: nacospassword: nacostx-service-group: hmall # 事务组名称service:vgroup-mapping: # 事务组与tc集群的映射关系hmall: "default"

不新建就是默认的命名空间
在这里插入图片描述
不划分就是默认的分组
在这里插入图片描述
也有不同的集群,我可能有不同的集群(部署在不同的服务器上)
在这里插入图片描述

一个事务组最好在一个集群当中

添加数据库表

seata的客户端在解决分布式事务的时候需要记录一些中间数据,保存在数据库中。因此我们要先准备一个这样的表。
将课前资料的seata-at.sql分别文件导入hm-trade、hm-cart、hm-item三个数据库中

测试

接下来就是测试的分布式事务的时候了。
我们找到trade-service模块下的com.hmall.trade.service.impl.OrderServiceImpl类中的createOrder方法,也就是下单业务方法。
将其上的@Transactional注解改为Seata提供的@GlobalTransactional:
在这里插入图片描述
@GlobalTransactional注解就是在标记事务的起点,将来TM就会基于这个方法判断全局事务范围,初始化全局事务。

我们重启trade-service、item-service、cart-service三个服务。再次测试,发现分布式事务的问题解决了!

XA模式

Seata支持四种不同的分布式事务解决方案:

  • XA
  • TCC
  • AT
  • SAGA

XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。

两阶段提交

正常情况:
在这里插入图片描述
异常情况:
在这里插入图片描述
一阶段:

  • 事务协调者通知每个事务参与者执行本地事务
  • 本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁

二阶段:

  • 事务协调者基于一阶段的报告来判断下一步操作
  • 如果一阶段都成功,则通知所有事务参与者,提交事务
  • 如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务
Seata的XA模型

在这里插入图片描述
RM一阶段的工作:

  1. 注册分支事务到TC
  2. 执行分支业务sql但不提交
  3. 报告执行状态到TC

TC二阶段的工作:

  1. TC检测各分支事务执行状态
  2. 如果都成功,通知所有RM提交事务
  3. 如果有失败,通知所有RM回滚事务

RM二阶段的工作:

  • 接收TC指令,提交或回滚事务
优缺点

优点:

  • 满足强一致性,满足ACID原则
  • 常用数据库都支持,实现简单,并且没有代码侵入

缺点:

  • 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
  • 依赖关系型数据库实现事务
实现步骤

编辑配置

seata:data-source-proxy-mode: XA

其次,我们要利用@GlobalTransactional标记分布式事务的入口方法:

AT模式

Seata的AT模型

AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。
在这里插入图片描述
阶段一RM的工作:

  • 注册分支事务
  • 记录undo-log(数据快照)
  • 执行业务sql并提交
  • 报告事务状态
    阶段二提交时RM的工作:
  • 删除undo-log即可
    阶段二回滚时RM的工作:
  • 根据undo-log恢复数据到更新前
流程图

在这里插入图片描述

优缺点

优点:

  • 性能较好,一个事物的提交不用等待其他事务的成功与失败

缺点:

  • 中间存在着端在的数据不一致现象,但是最终会保证一致性

相关文章:

【SpringCloud】从入门到精通(下)

网关与路由 什么是网关&#xff1f;顾明思议&#xff0c;网关就是网络的关口。数据在网络间传输&#xff0c;从一个网络传输到另一网络时就需要经过网关来做数据的路由和转发以及数据安全的校验。 现在前端不能请求各个微服务地址&#xff0c;只能去请求网关 网关可以做安全控…...

深入探索 C++23:特性测试与编译器支持

文章目录 一、C23 新特性概览&#xff08;一&#xff09;语言特性&#xff08;二&#xff09;标准库特性 二、特性测试程序三、主流编译器支持情况&#xff08;一&#xff09;GCC&#xff08;二&#xff09;Clang&#xff08;三&#xff09;MSVC 四、开发者建议&#xff08;一&…...

Electron 应用太重?试试 PakePlus 轻装上阵

Electron 作为将 Web 技术带入桌面应用领域的先驱框架&#xff0c;让无数开发者能够使用熟悉的 HTML、CSS 和 JavaScript 构建跨平台应用。然而&#xff0c;随着应用规模的扩大&#xff0c;Electron 应用的性能问题逐渐显现——内存占用高、启动速度慢、安装包体积庞大&#xf…...

驱动程序无法通过使用安全套接字层(SSL)加密与 SQL Server 建立安全连接

驱动程序无法通过使用安全套接字层(SSL)加密与 SQL Server 建立安全连接 原因描述 项目中有使用到 SQL Server 数据库, 在启动项目时, 出现报错信息: 【驱动程序无法通过使用安全套接字层(SSL)加密与 SQL Server 建立安全连接。错误:“The server selected protocol version…...

Java 设计模式:原型模式详解

Java 设计模式&#xff1a;原型模式详解 原型模式&#xff08;Prototype Pattern&#xff09;是一种创建型设计模式&#xff0c;它通过复制现有对象来创建新对象&#xff0c;而无需依赖其具体类。这种模式特别适合创建复杂对象或需要频繁创建相似对象的场景。本文将详细介绍原…...

NLP高频面试题(三十七)——大模型训练和推理的显存估计

在训练和推理大型语言模型时,显存(GPU 内存)的需求是一个关键考虑因素。准确估计这些需求有助于选择合适的硬件配置,确保模型高效运行。 推理阶段的显存需求 在推理过程中,显存主要用于存储模型权重和中间激活值。模型权重的显存需求可以通过以下公式估算: 模型权重…...

PHP 阿里云oss 使用指南

1.介绍 把图片放到阿里云上的空间上&#xff0c;可以使用cdn加速。 可以在程序里直接调用 要使用阿里云 oss sdk &#xff0c;请先到阿里云下载 或用 copmposer 安装 相关链接&#xff1a; 安装OSS PHP SDK_对象存储(OSS)-阿里云帮助中心 composer require aliyuncs/oss…...

leetcode_面试题 02.07. 链表相交_java

面试题 02.07. 链表相交https://leetcode.cn/problems/intersection-of-two-linked-lists-lcci/ 1、题目 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点&#xff0c;返回 null 。 图示两个链表在节点 c…...

LeetCode 3375.使数组的值全部为 K 的最少操作次数:O(1)空间——排序+一次遍历

【LetMeFly】3375.使数组的值全部为 K 的最少操作次数&#xff1a;O(1)空间——排序一次遍历 力扣题目链接&#xff1a;https://leetcode.cn/problems/minimum-operations-to-make-array-values-equal-to-k/ 给你一个整数数组 nums 和一个整数 k 。 如果一个数组中所有 严格…...

紫光展锐5G SoC T8300:影像升级,「定格」美好世界

影像能力已成为当今衡量智能手机性能的重要标尺之一。随着消费者对手机摄影需求日益提升&#xff0c;手机厂商纷纷在影像硬件和算法上展开激烈竞争&#xff0c;力求为用户带来更加出色的拍摄体验。 紫光展锐专为全球主流用户打造的畅享影音和游戏体验的5G SoC——T8300&#x…...

java基础 关键字static

static static使用简介static结合类的生命周期1.加载2.链接(1) 验证&#xff08;Verification&#xff09;(2) 准备&#xff08;Preparation&#xff09;(3) 解析&#xff08;Resolution&#xff09; 3. 初始化4.使用5.卸载总结 staic作用总结静态变量静态代码块静态方法静态内…...

大数据学习(105)-大数据组件分析

&#x1f34b;&#x1f34b;大数据学习&#x1f34b;&#x1f34b; &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 用力所能及&#xff0c;改变世界。 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4dd;支持一…...

Spark运行

一文读懂Spark&#xff1a;从核心概念到实战编程 在大数据处理领域&#xff0c;Spark凭借其高效的计算能力和灵活的架构脱颖而出。今天&#xff0c;就来和大家深入聊聊Spark&#xff0c;帮助初学者快速入门。Spark采用经典的master - slave结构。Driver如同master&#xff0c;…...

在macOS的docker中如何安装及运行ROS2

1、macOS环境及版本 2、docker for macos版本 3、拉取ROS2镜像 docker pull ros:iron 4、查看容器 docker images 5、启动 ROS2 容器 docker run -it --rm ros:iron -it &#xff1a;以交互模式运行容器。 --rm &#xff1a;退出时自动删除容器&#xff08;测试时推荐&am…...

FFmpeg安装和使用

1. 安装与环境配置 Windows # 方法1&#xff1a;官网下载预编译二进制包 https://ffmpeg.org/download.html#build-windows 解压后添加bin目录到系统PATH# 方法2&#xff1a;通过Chocolatey安装 choco install ffmpegmacOS # 使用Homebrew安装 brew install ffmpegLinux # …...

基于多模态大模型的ATM全周期诊疗技术方案

基于多模态大模型的ATM全周期诊疗技术方案 1. 数据预处理模块 算法1:多模态数据融合伪代码 def multimodal_fusion(data_dict):# 输入:包含MRI、EEG、实验室指标的字典# 输出:对齐后的张量序列# 模态对齐aligned_data = temporal_alignment(data_dict,sampling_rate...

写时复制Copy-on-Write(COW)

简单理解写时复制 读的时候&#xff0c;直接访问原对象。 写的时候&#xff0c;对复制原对象&#xff0c;对副本进行写操作&#xff0c;最后将副本替换原对象。 写时复制多用于读多写少的场景&#xff0c;因为写操作是用悲观锁进行的&#xff0c;如果写的场景多&#xff0c;…...

S7-1200 PLC热电偶和热电阻模拟量模块

热电偶和热电阻模拟量模块 S7-1200 PLC有专用用于对温度进行采集的热电偶模块SM1231 TC和SM 1231RTD。热电偶模块有4AI和8AI两种&#xff0c;下面以SM1231 TC 4AI为例看一下接线图。 该模块一共有4个通道&#xff0c;每个通道有两个接线端子&#xff0c;比如0&#xff0c;0-。…...

ffmpeg函数简介(封装格式相关)

文章目录 &#x1f31f; 前置说明&#xff1a;FFmpeg 中 AVFormatContext 是什么&#xff1f;&#x1f9e9; 1. avformat_alloc_context功能&#xff1a;场景&#xff1a; &#x1f9e9; 2. avformat_open_input功能&#xff1a;说明&#xff1a;返回值&#xff1a; &#x1f9…...

操作数组的工具类

Arrays 它里面的每一个方法基本上都是static静态修饰的&#xff0c;如果想要调用里面的方法&#xff0c;不需要创建对象&#xff0c;直接用类名.就可以了 操作数组的工具类 方法&#xff1a; public static String toString&#xff08;数组&#xff09; 把数组拼接成…...

小刚说C语言刷题——第19讲 循环之continue和break

在循环中&#xff0c;当我们得到想要的答案时&#xff0c;这时我们可能要提前结束循环&#xff0c;这个时候我们就会用到break。而我们有时需要结束某一次循环时&#xff0c;我们可以用continue。 1.break语句 (1)在循环中想要提前终止循环&#xff0c;要用break。 (2)语法格…...

FairMOT复现过程中cython_bbox库问题

cython_bbox库就该这么安装_cython-bbox库就应该-CSDN博客...

记录学习的第二十四天

还是每日一题。 题解很巧&#xff0c;我根本想不到。 class Solution { public: int minOperations(vector<int>& nums, int k) { int count; int mnnums[0]; //接下来查找nums数组中最小值 for(int i1;i<nums.size();i) { if(nums[i]<mn) { mnnums[i]; } } …...

Kubernetes 入门篇之网络插件 calico 部署与安装

在运行kubeadm init 和 join 命令部署好master和node节点后&#xff0c;kubectl get nodes 看到节点都是NotReady状态&#xff0c;这是因为没有安装CNI网络插件。 kubectl get nodes NAME STATUS ROLES AGE VERSION k8s-master Not…...

HTTP 压力测试工具autocannon(AI)

简介 autocannon 是一款基于 Node.js 的高性能 HTTP 压力测试工具&#xff0c;适用于评估 Web 服务的并发处理能力和性能瓶颈。 一、工具特点 高性能‌&#xff1a;利用 Node.js 异步非阻塞机制模拟高并发请求‌。‌实时监控‌&#xff1a;测试过程中动态展示请求统计和性能…...

【面试】封装、继承、多态的具象示例 模板编程的理解与应用场景 链表适用的场景

文章目录 C面试&#xff1a;封装、继承、多态的具象示例1. 封装 (Encapsulation)2. 继承 (Inheritance)3. 多态 (Polymorphism)综合示例&#xff1a;封装、继承、多态 C模板编程的理解与应用场景我对模板编程的理解C中最常用的模板编程场景1. STL (标准模板库)2. 通用容器实现3…...

机器学习02——概要

一、简介 机器学习是一门在没有明确编程的情况下让计算机学习的科学。 监督学习是有目标的&#xff0c;输入数据对应明确的输出&#xff1b;无监督学习则是“探索”型的&#xff0c;模型的目标是从数据中发现潜在的模式或结构&#xff0c;而不需要预先知道标签。 二、机器学…...

常用的网络安全靶场、工具箱

转载&#xff1a;https://blog.csdn.net/zjzqxzhj/article/details/137945444 打CTF很好玩。可以试一下 1.CTF在线工具 1、CTF在线工具箱&#xff1a;http://ctf.ssleye.com/ 包含CTF比赛中常用的编码、加解密、算法。 2、CTF加解密工具箱&#xff1a;http://www.atoolbox.…...

excel中的VBA指令示例(一)

示例注释&#xff1a; Sub 宏1() sub是宏开头&#xff0c;宏1是宏的名称&#xff0c;自定义&#xff0c;在按钮中可指定用某个宏 后面是注释 Sheets("装配材料").Select ‘选择表 装配材料 Ce…...

神经网络 | 基于脉冲耦合神经网络PCNN图像特征提取与匹配(附matlab代码)

内容未发表论文基于脉冲耦合神经网络(PCNN)的图像特征提取与匹配研究 摘要 本文提出一种基于脉冲耦合神经网络(Pulse-Coupled Neural Network, PCNN)的图像特征提取与匹配方法。通过模拟生物视觉皮层神经元的脉冲同步发放特性,PCNN能够有效捕捉图像纹理与边缘特征。实验表…...

Linux 内核中的 TCP 早期多路分解机制解析

一、引言 在现代高性能网络环境中,Linux 内核需要快速处理大量的 TCP 数据包,同时保持低延迟和高吞吐量。为了实现这一目标,Linux 内核引入了 早期多路分解(Early Demultiplexing) 机制。这种机制允许内核在数据包进入传输层之前,快速找到对应的套接字(socket)并关联数…...

Yalmip工具箱(3)——错误类型

在yalmip中&#xff0c;不可避免地我们会遇到求解出问题的情况&#xff0c;理解和处理错误信息是至关重要的环节。在这里我们查看yalmip的所有错误类型&#xff08;详细见 yalmiperror.m 函数&#xff09; 函数概述 yalmiperror函数的主要作用是根据YALMIP产生的错误代码&…...

【KWDB 创作者计划】_KWDB:开源引领数据库创新变革

在数字化浪潮汹涌澎湃的当下&#xff0c;数据已然成为驱动各行各业发展的核心要素。数据库作为数据管理的关键工具&#xff0c;其性能、功能以及开放性&#xff0c;对企业和社会的数字化进程起着举足轻重的作用。KWDB&#xff0c;作为数据库领域的一颗璀璨新星&#xff0c;正以…...

HarmonyOS学习 实验八:显式动画与属性动画的实现

鸿蒙系统动画开发实战&#xff1a;显式动画与属性动画的探索 引言 在鸿蒙系统的开发过程中&#xff0c;动画效果是提升用户体验的重要一环。通过巧妙运用动画&#xff0c;可以使应用界面更加生动、交互更加流畅。鸿蒙系统提供了丰富的动画开发能力&#xff0c;其中显式动画和…...

高校智慧能源系统解决方案:推动绿色校园建设的智能化实践

高校智慧能源系统解决方案&#xff1a;推动绿色校园建设的智能化实践 一、建设背景&#xff1a;政策驱动与绿色发展需求 为响应国家“碳达峰、碳中和”战略目标&#xff0c;教育部印发《绿色低碳发展国民教育体系建设实施方案》&#xff0c;明确提出需完善校园能源管理体系&a…...

win日志

以第一个为例子 打开后&#xff0c;右上角&#xff08;将所有事件另存为xx)然后一般写今天的日期&#xff0c;进行备份 然后选择下语言即可 日志备份时间的选择&#xff08;根据实际情况选择日志时间&#xff09; 点击右侧事件属性&#xff0c;然后xml视图即可 常见的安全事件…...

嵌入式开发之51单片机入门(一)与LED灯的故事

得而不惜就该死。 --小泽 继续傻冒开始&#xff0c;这次的傻冒之旅是关于嵌入式的51单片机开发&#xff0c;这个系列只讲程序开发逻辑&#xff0c;如需初始环境安装配置&#xff0c;建议移步B站江协科技大佬&#xff0c;本系列也是对大佬所讲内容的复刻&#xff0c;同时添加一…...

TCP 与 UDP

TCP 与 UDP 的区别&#xff08;重要&#xff09; 是否面向连接&#xff1a;UDP 在传送数据之前不需要先建立连接。而 TCP 提供面向连接的服务&#xff0c;在传送数据之前必须先建立连接&#xff0c;数据传送结束后要释放连接。是否是可靠传输&#xff1a;远地主机在收到 UDP 报…...

Linux:进程地址空间

在讲述本篇文章之前&#xff0c;我们先来看一段代码。 从上图输出可以看到&#xff0c;我们的子进程继承了父进程的全局变量val&#xff0c;当子进程中的val产生了修改时&#xff0c;父进程的val值并没有变化&#xff0c;但父子进程在打印val的地址时&#xff0c;会发现val的地…...

【Linux】Linux 操作系统 - 03 ,初步指令结尾 + shell 理解

文章目录 前言一、打包和压缩二、有关体系结构 (考)面试题 三、重要的热键四、shell 命令及运行原理初步理解五、本节命令总结总结 前言 本篇文章 , 笔者记录的笔记内容包含 : 基础指令 、重要热键 、shell 初步理解 、权限用户的部分问题 。 内容皆是重要知识点 , 需要认真理…...

华为数通不同级别的认证路径和要求是什么?

一、认证路径 HCIA&#xff1a;无需前置认证&#xff0c;通过一门考试&#xff08;代码H12-811&#xff09;即可拿证。 HCIP&#xff1a;建议先通过HCIA-Datacom&#xff0c;再选择子方向&#xff08;如高级路由、安全&#xff09;。 HCIE&#xff1a;最好有hcia/hcip的基础…...

电子电气架构 --- 新能源汽车电子电气系统功能需求

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 周末洗了一个澡,换了一身衣服,出了门却不知道去哪儿,不知道去找谁,漫无目的走着,大概这就是成年人最深的孤独吧! 旧人不知我近况,新人不知我过…...

《从单体到分布式:一个订单系统的架构升级》

1. 问题爆发&#xff1a;单体架构的极限​​ 原来的订单系统长这样&#xff1a; ​​技术栈​​&#xff1a;SpringBoot MyBatis MySQL&#xff08;主从&#xff09;​​部署​​&#xff1a;单机跑所有模块&#xff08;订单、支付、库存&#xff09;​​痛点​​&#xff…...

琴键上的强化学习:让机器人在真实世界里弹钢琴!

在科技飞速发展的今天&#xff0c;机器人的能力边界不断拓展。想象一下&#xff0c;机器人坐在钢琴前&#xff0c;行云流水地弹奏出美妙乐章&#xff0c;这不再是科幻电影里的场景。近日&#xff0c;科研人员在机器人弹钢琴领域取得了突破性进展&#xff0c;让我们一同走进这个…...

【Unity网络编程知识】C#的 Http相关类学习

1、搭建HTTP服务器 使用别人做好的HTTP服务器软件&#xff0c;一般作为资源服务器时使用该方式&#xff08;学习阶段建议使用&#xff09;自己编写HTTP服务器应用程序&#xff0c;一般作为Web服务器或者短连接游戏服务器时使用该方式&#xff08;工作后由后端程序员来做&#…...

【算法学习】链表篇:链表的常用技巧和操作总结

算法学习&#xff1a; https://blog.csdn.net/2301_80220607/category_12922080.html?spm1001.2014.3001.5482 前言&#xff1a; 在各种数据结构中&#xff0c;链表是最常用的几个之一&#xff0c;熟练使用链表和链表相关的算法&#xff0c;可以让我们在处理很多问题上都更加…...

【前端笔记】CSS预处理语言 LESS

官网&#xff1a; Less 快速入门 | Less.js 中文文档 - Less 中文网 LESS很多地方可以比CSS简写很多 安装 npm install -g less 核心优势&#xff1a;变量、嵌套、混合、运算功能让CSS更容易维护&#xff0c;比原生CSS更加简洁高效 1. 变量&#xff08;Variables&#xff09…...

Oracle 查看后台正在执行的 SQL 语句

在 Oracle 数据库中&#xff0c;要查看后台正在执行的 SQL 语句&#xff0c;可以通过查询动态性能视图&#xff08;Dynamic Performance Views&#xff09;或使用监控工具来实现。 1. 查询动态性能视图 (1) 查看当前活跃会话及其执行的 SQL 使用 v$session 和 v$sql 视图关联…...

强化学习原理二 BasicConcepts

状态&#xff0c;State 状态空间&#xff0c;State Space 行动&#xff0c;Action状态转换&#xff0c;state transition策略&#xff0c;Policy 用数组或者矩阵表示这样一个策略 奖励&#xff0c;Reward 不确定的话&#xff0c;表格就不适用了。这个时候就要用数学来表示&…...

【机密计算顶会解读】13:CAGE:通过 GPU 扩展补充 Arm CCA

导读&#xff1a;本文介绍GAGE&#xff0c;利用Arm CCA中的现有硬件安全特性来确保敏感数据的安全性&#xff0c;支持GPU加速的机密计算&#xff0c;在实际平台上的平均性能开销仅为 2.45%&#xff0c;在保持高性能的同时&#xff0c;提供了良好的数据安全保护&#xff0c;且其…...