如何系列 如何使用Resilience4j提高应用弹性和容错

news2024/11/28 20:36:56

文章目录

    • 背景
    • 简介
    • Maven
    • 重试器
      • 注解式
      • 编程式
      • 配置
      • 事件监听
      • 指标监控
      • 健康检查
    • 速率限制
      • 注解式
      • 编程式
      • 配置
      • 事件监听
      • 指标监控
      • 动态修改配置
    • 断路器
      • 注解式
      • 配置
    • 舱壁
      • 注解式
    • 时间限制器
    • 多组件配合使用
      • 最佳实践
    • 配置
    • 参考:

背景

在应用程序开发的过程中,特别是在构建基于微服务的应用时,我们常常会在实时运行环境中遭遇各种偏差和挑战。有时候,这些偏差可能表现为响应的延迟网络通信中断REST调用的失败,甚至可能是因为突发的大量请求而导致的服务失效等等。为了有效地应对这些潜在的异常情况,我们必须在我们的应用程序中嵌入弹性和容错机制,以确保我们的系统能够在不确定的环境下保持稳定性和可靠性。通过采取这些措施,我们能够提高应用程序的鲁棒性,确保它在面对各种不可预测的情况时依然能够持续提供高质量的服务。

简介

官网:https://resilience4j.readme.io/

Github:https://github.com/resilience4j/resilience4j

Resilience4j是一个轻量级容错库,灵感来自 Netflix Hystrix,但专为函数式编程而设计。

Resilience4j提供装饰器,以通过断路器、速率限制器、重试或舱壁增强任何功能接口、lambda 表达式或方法引用。您可以在任何函数式接口、lambda 表达式或方法引用上堆叠多个装饰器。

以下是关于Resilience4j的一些要点:

  1. 断路器模式(Circuit Breaker)当操作连续失败时快速失败或执行默认操作,Resilience4j实现了断路器模式,它可以在系统出现故障时自动切换到开启状态,阻止请求继续发送到故障的组件。这有助于防止故障蔓延到整个系统,保持系统的可用性。resilience4j-circuitbreaker
  2. 重试器(Retry)自动重试失败的操作,库中内置了重试机制,允许在请求失败时进行重试。可以配置重试的次数、延迟和指数退避等策略,以期待后续的尝试可以成功。resilience4j-retry
  3. 限流器(Rate Limiter)限制一定时间内调用操作的次数,Resilience4j还提供了限流器功能,用于控制对某个操作或服务的请求速率。这有助于防止过多的请求同时涌入系统,导致系统过载。resilience4j-ratelimiter
  4. 时间限制器(Timeout)设置调用操作的时间限制,通过设置操作的最大执行时间,可以避免某个操作耗时过长而阻塞其他请求。Resilience4j可以在指定的时间内取消或中断执行超时的操作。resilience4j-timelimiter
  5. 舱壁(Bulkhead)限制并发操作的数量和保护请求隔离,该功能允许您限制并发请求的数量,确保资源在不同的任务之间得到合理的分配。这有助于防止一个失败的请求影响到其他请求。resilience4j-bulkhead
  6. 结果缓存(Cache): 存储昂贵的操作的结果resilience4j-cache
  7. 事件监听与监控:Resilience4j允许您注册事件监听器,以便在断路器状态变化、重试等事件发生时进行通知或记录。这有助于监控系统的运行状况以及故障情况。

简单示例

// 待包装方法
String result = service.sayHelloWorld(param1);
// 包装方法
Supplier<String> supplier = () -> service.sayHelloWorld(param1);
String result = Decorators.ofSupplier(supplier) // 包装方法
  .withBulkhead(Bulkhead.ofDefaults("name")) // 加装舱壁器
  .withCircuitBreaker(CircuitBreaker.ofDefaults("name"))// 加装断路器
  .withRetry(Retry.ofDefaults("name")) // 加装重试器
  .withFallback(asList(CallNotPermittedException.class, BulkheadFullException.class),  // 加装降级方法
      throwable -> "Hello from fallback")
  .get()

Resilience4j 2 需要 Java 17。

https://resilience4j.readme.io/v1.7.0/docs

Maven

首先,我们需要将目标模块添加到pom.xml中(例如,在这里我们添加断路器)

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-circuitbreaker</artifactId>
    <version>0.12.1</version>
</dependency>

Spring Boot2中添加如下依赖

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
    <version>1.7.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

所有模块及其最新版本都可以在Maven Central上找到。

下面我们的部分示例会包含注解式和编程式。

重试器

自动重试失败的调用

设想微服务“A”在其运作过程中依赖于另一微服务“B”。假设微服务“B”的运行质量存在缺陷,其成功率仅为50-60%。然而,故障的根源可以多种多样,例如服务不可用、有缺陷的服务间歇性响应、网络问题等。在这种情形下,如果微服务“A”能够对失败的调用进行自动重试,将调用的成功机会提升至有益的程度。

实现目标

我们的应用程序在尝试获取某个API时,时常因内部问题遭遇调用失败。我们留意到在经过几轮尝试后,该API通常会恢复稳定,返回预期的响应。我们通过以下逻辑进行优化:

  • 当出现HttpServerErrorException异常时,进行3次重试。
  • 在每次重试后等待2秒。
  • 若经过第三次重试仍然失败,返回缓存值以确保数据可用性。

注解式

示例代码如下

@Service
@Slf4j
public class UserService {
    // 制定策略 和 降级方法
    @Retry(name = "getUserInfo", fallbackMethod = "getUserInfoFallback")
    public String getUserInfoById(String id) {

        log.info("开始请求远程服务");
        // 模拟请求远端异常
        if (id.equals("1")) {
            throw new HttpServerErrorException(HttpStatus.BAD_GATEWAY);
        }
        log.info("结束请求远程服务");
        return id;
    }
	
    // 降级方法
    private String getUserInfoFallback(String id, Exception exception) {
        log.error("远程服务异常,启用降级方法,id:{}", id, exception);
        return "getUserInfoFallbackFromLocalCache";
    }
}

测试日志如下

2023-08-09 21:03:42.801 [nio-8080-exec-1] com.laker.service.UserService: 开始请求远程服务
2023-08-09 21:03:44.826 [nio-8080-exec-1] com.laker.service.UserService: 开始请求远程服务
2023-08-09 21:03:46.829 [nio-8080-exec-1] com.laker.service.UserService: 开始请求远程服务
2023-08-09 21:03:46.829 [nio-8080-exec-1] com.laker.service.UserService: 远程服务异常,启用降级方法,id:1
org.springframework.web.client.HttpServerErrorException: 502 BAD_GATEWAY
	at com.laker.service.UserService.getUserInfoById(UserService.java:23) ~[classes/:na]
	...

默认情况下

  • Resilience4J 将尝试调用带注释的方法 3 次,每次调用之间的等待时间为 500 毫秒。

  • 如果没有成功调用,resilience4j 将调用后备方法并使用其返回值。

  • 但要小心:您需要确保重试的操作是幂等的,否则您最终可能会得到损坏的数据。

配置

resilience4j:
  retry:
    instances:
      getUserInfo:
        # 最大重试次数值
        max-attempts: 3
        # 等待下一次尝试的长值
        waitDuration: 2s
        # 重试异常类列表
        retry-exceptions:
          - org.springframework.web.client.HttpServerErrorException

降级方法/回退(Fallback)方法/备选方法

针对所有模块中(重试,断路器等等)的降级方法

回退方法机制的工作原理类似于 try/catch 块。如果配置了回退方法,则每个执行都会转发到后备方法执行器。回退方法执行器正在搜索可以处理异常的最佳匹配回退方法。

即如果把上面示例中的异常改为模拟抛出RuntimeException,虽然没有了重试了但是还是会走降级方法。

public String getUserInfoById(String id) {
		...
        if (id.equals("1")) {
            // 只修改了这里
            throw new RuntimeException("123");
        }
        ...
        return id;
    }

测试日志如下

2023-08-09 21:06:33.530 [nio-8080-exec-1] com.laker.service.UserService: 开始请求远程服务
2023-08-09 21:06:33.531 [nio-8080-exec-1] com.laker.service.UserService: 远程服务异常,启用降级方法,id:1
java.lang.RuntimeException: 123
	at com.laker.service.UserService.getUserInfoById(UserService.java:21) ~[classes/:na]
	...

编程式

    @Test
    public void test(){
        UserService userService = mock(UserService.class);
        RetryConfig config = RetryConfig.custom().maxAttempts(3).build();
        RetryRegistry registry = RetryRegistry.of(config);
        Retry retry = registry.retry("my");
        Function<Integer, Void> decorated
                = Retry.decorateFunction(retry, (Integer s) -> {
            userService.getUserInfoById("1");
            return null;
        });
        when(userService.getUserInfoById(anyString())).thenThrow(new RuntimeException());
        try {
            decorated.apply(1);
            fail("Expected an exception to be thrown if all retries failed");
        } catch (Exception e) {
            verify(userService, times(3)).getUserInfoById(anyString());
        }
    }

配置

配置参数默认值描述
maxAttempts3最大重试次数
waitDuration500[ms]固定重试间隔
intervalFunctionnumberOfAttempts -> waitDuration用来改变重试时间间隔,可以选择指数退避或者随机时间间隔
retryOnResultPredicateresult -> false自定义结果重试规则,需要重试的返回true
retryOnExceptionPredicatethrowable -> true自定义异常重试规则,需要重试的返回true
retryExceptionsempty需要重试的异常列表
ignoreExceptionsempty需要忽略的异常列表
resilience4j:
  retry:
    configs:
      default:
      maxRetryAttempts: 3
      waitDuration: 10s
      enableExponentialBackoff: true    # 是否允许使用指数退避算法进行重试间隔时间的计算
      expontialBackoffMultiplier: 2     # 指数退避算法的乘数
      enableRandomizedWait: false       # 是否允许使用随机的重试间隔
      randomizedWaitFactor: 0.5         # 随机因子
      resultPredicate: com.example.resilience4j.predicate.RetryOnResultPredicate    
      retryExceptionPredicate: com.example.resilience4j.predicate.RetryOnExceptionPredicate
      retryExceptions:
        - com.example.resilience4j.exceptions.BusinessBException
        - com.example.resilience4j.exceptions.BusinessAException
        - io.github.resilience4j.circuitbreaker.CallNotPermittedException
      ignoreExceptions:
        - io.github.resilience4j.circuitbreaker.CallNotPermittedException
      instances:
        backendA:
          baseConfig: default
          waitDuration: 5s
        backendB:
          baseConfig: default
          maxRetryAttempts: 2   

注意指数退避和随机间隔不能同时启用。

事件监听

注册事件侦听器。事件侦听器捕获过程中发生的不同事件并执行特定操作,例如相应地记录或处理事件。

    @Autowired
    private RetryRegistry retryRegistry;

    @PostConstruct
    public void postConstruct() {
        io.github.resilience4j.retry.Retry.EventPublisher eventPublisher = retryRegistry.retry("getUserInfo").getEventPublisher();
        eventPublisher.onEvent(event -> System.out.println("Retry - On Event. Event Details: " + event));
        eventPublisher.onSuccess(event -> System.out.println("Retry - On Success. Event Details: " + event));
        eventPublisher.onError(event -> System.out.println("Retry - On Failure. Event Details: " + event));
        eventPublisher.onRetry(event -> System.out.println("Retry - On Retry. Event Details: " + event));
    }
    ...

    @Retry(name = "getUserInfo", fallbackMethod = "getUserInfoFallback")
    public String getUserInfoById(String id) {
    ....
2023-08-09 21:47:30.114  INFO 4612 --- [nio-8080-exec-1] com.laker.service.UserService            : 开始请求远程服务
Retry - On Event. Event Details: 2023-08-09T21:47:30.114+08:00[Asia/Shanghai]: Retry 'getUserInfo', waiting PT2S until attempt '1'. Last attempt failed with exception 'org.springframework.web.client.HttpServerErrorException: 502 BAD_GATEWAY'.
Retry - On Retry. Event Details: 2023-08-09T21:47:30.114+08:00[Asia/Shanghai]: Retry 'getUserInfo', waiting PT2S until attempt '1'. Last attempt failed with exception 'org.springframework.web.client.HttpServerErrorException: 502 BAD_GATEWAY'.
2023-08-09 21:47:32.116  INFO 4612 --- [nio-8080-exec-1] com.laker.service.UserService            : 开始请求远程服务
Retry - On Event. Event Details: 2023-08-09T21:47:32.116+08:00[Asia/Shanghai]: Retry 'getUserInfo', waiting PT2S until attempt '2'. Last attempt failed with exception 'org.springframework.web.client.HttpServerErrorException: 502 BAD_GATEWAY'.
Retry - On Retry. Event Details: 2023-08-09T21:47:32.116+08:00[Asia/Shanghai]: Retry 'getUserInfo', waiting PT2S until attempt '2'. Last attempt failed with exception 'org.springframework.web.client.HttpServerErrorException: 502 BAD_GATEWAY'.
2023-08-09 21:47:34.128  INFO 4612 --- [nio-8080-exec-1] com.laker.service.UserService            : 开始请求远程服务
Retry - On Event. Event Details: 2023-08-09T21:47:34.128+08:00[Asia/Shanghai]: Retry 'getUserInfo' recorded a failed retry attempt. Number of retry attempts: '3'. Giving up. Last exception was: 'org.springframework.web.client.HttpServerErrorException: 502 BAD_GATEWAY'.
Retry - On Failure. Event Details: 2023-08-09T21:47:34.128+08:00[Asia/Shanghai]: Retry 'getUserInfo' recorded a failed retry attempt. Number of retry attempts: '3'. Giving up. Last exception was: 'org.springframework.web.client.HttpServerErrorException: 502 BAD_GATEWAY'.
2023-08-09 21:47:34.129 ERROR 4612 --- [nio-8080-exec-1] com.laker.service.UserService            : 远程服务异常,启用降级方法,id:1

org.springframework.web.client.HttpServerErrorException: 502 BAD_GATEWAY
	at com.laker.service.UserService.getUserInfoById(UserService.java:39) ~[classes/:na]
	...

事件:http://localhost:8080/actuator/retryevents

指标监控

management:
  endpoint:
    health:
      show-details: ALWAYS
  endpoints:
    web:
      exposure:
        include: "*"

CircuitBreaker、Retry、RateLimiter、Bulkhead 和 TimeLimiter 指标会自动发布在指标端点上。请参阅执行器指标文档。

http://localhost:8080/actuator/metrics

要检索指标,请向 发出 GET 请求。 例如:/actuator/metrics/{metric.name}

http://localhost:8080/actuator/metrics/resilience4j.retry.calls

{
    "name":"resilience4j.retry.calls",
    "description":"The number of successful calls after a retry attempt",
    "baseUnit":null,
    "measurements":[
        {
            "statistic":"COUNT",
            "value":2
        }
    ],
    "availableTags":[
        {
            "tag":"kind",
            "values":[
                "successful_without_retry",
                "successful_with_retry",
                "failed_with_retry",
                "failed_without_retry"
            ]
        },
        {
            "tag":"name",
            "values":[
                "getUserInfo"
            ]
        }
    ]
}

健康检查

Spring Boot Actuator 健康信息可用于检查正在运行的应用程序的状态。监控软件经常使用它来提醒某人生产系统是否存在严重问题。
默认情况下,CircuitBreaker 或 RateLimiter 运行状况指示器处于禁用状态,但您可以通过配置启用它们。运行状况指示器被禁用,因为当断路器打开时,应用程序状态为“关闭”。这可能不是您想要实现的目标。

management.health.circuitbreakers.enabled: true
management.health.ratelimiters.enabled: true

resilience4j.circuitbreaker:
  configs:
    default:
      registerHealthIndicator: true

resilience4j.ratelimiter:
  configs:
    instances:
      registerHealthIndicator: true

闭合的 CircuitBreaker 状态映射到 UP,打开状态映射到 DOWN,半打开状态映射到 UNKNOWN。

速率限制

速率限制是一种用于控制系统或 API 请求流的技术。想象一下,您有一个受欢迎的网站或 Web 服务,收到大量请求。如果没有速率限制,您的系统可能会不堪重负,导致响应时间变慢、服务器负载增加,甚至崩溃。速率限制对特定时间范围内可以发出的请求数量进行限制,确保系统保持稳定和可用。它有助于防止滥用、防止 DDoS 攻击并允许公平使用资源。通过实施速率限制,您可以有效管理系统流量、保持性能并为用户提供更好的体验。

速率限制有两种主要方法:client-sideserver-side速率限制。在客户端速率限制中,客户端控制其向外部服务发出的请求数量。另一方面,服务器端速率限制对传入请求设置限制。了解这些方法之间的差异有助于保持系统稳定性并优化性能。

服务器端速率限制需要诸如缓存和多个服务器实例之间的协调之类的东西,这是resilience4j不支持的。对于服务器端速率限制,有 API 网关和 API 过滤器。

resilience4j常用于客户端速率限制

bucket4j常用于服务端速率限制

 <dependency>
        <groupId>com.giffing.bucket4j.spring.boot.starter</groupId>
        <artifactId>bucket4j-spring-boot-starter</artifactId>
</dependency>

速率限制器的概念

  • 能够限制对方法的访问,在定义的时间段内限制指定数量的调用
  • 如果达到阈值,我们可以在实际尝试调用该方法之前设置最大等待时间
  • 如果仍然达到阈值,则抛出 RequestNotPermission 并执行后备方法

例如,我们需求如下:

  • 将我们的 API 使用限制为每秒2次调用
  • 如果给定时间段内 API 使用率高于阈值,后续调用将等待 1 秒
  • 等待一段时间后,如果API使用率仍然高于阈值,则返回缓存值(或者向用户返回 429)

注解式

@Service
@Slf4j
public class UserService {
    // 制定策略 和 降级方法
    @SneakyThrows
    @RateLimiter(name = "getUserInfo", fallbackMethod = "getUserInfoFallback")
    public String getUserInfoById(String id) {
        log.info("开始请求远程服务");
        log.info("结束请求远程服务");
        return id;
    }

    // 降级方法
    private String getUserInfoFallback(String id, Exception exception) {
         if (exception instanceof RequestNotPermitted){
            log.error("触发限流,启用降级方法,id:{}", id, exception);
        }
        return "getUserInfoFallbackFromLocalCache";
    }
}

配置

resilience4j:
  ratelimiter:
    instances:
      getUserInfo:
        # 周期内可用的令牌数
        limit-for-period: 2
        # 刷新周期
        limit-refresh-period: 1s
        # 等待许可持续时间
        timeout-duration: 1s

向用户返回 429

这里一般情况下使用bucket4j而不是resilience4j

    @RateLimiter(name = "getUserInfo")
    public String getUserInfoById(String id) {
        log.info("开始请求远程服务");
        log.info("结束请求远程服务");
        return id;
    }

@ControllerAdvice
public class ApiExceptionHandler {
	@ExceptionHandler({ RequestNotPermitted.class })
	@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
	public void handleRequestNotPermitted() {
	}
}

编程式

在 Resilience4j 中,您可以灵活地以编程方式创建速率限制器实例。这意味着您可以根据应用程序的要求动态配置和自定义速率限制器,从而对请求的管理和限制方式进行精细控制。

@Configuration
public class RateLimiterConfiguration {

    @Autowired
    private RateLimiterRegistry rateLimiterRegistry;

    @Bean
    public RateLimiter rateLimitWithCustomConfig() {
        RateLimiterConfig customConfig = RateLimiterConfig.custom()
                .limitForPeriod(2)
                .limitRefreshPeriod(Duration.of(5, ChronoUnit.SECONDS))
                .timeoutDuration(Duration.of(5, ChronoUnit.SECONDS))
                .build();

        return rateLimiterRegistry.rateLimiter("customRateLimiterConfig", customConfig);
    }
}

@RateLimiter(name = "customRateLimiterConfig")
public Movie getUserInfoById(String Id) {
}

配置

配置参数默认值描述
timeoutDuration5[s]线程等待权限的默认等待时间
limitRefreshPeriod500[ns]权限刷新的时间,每个周期结束后,RateLimiter将会把权限计数设置为limitForPeriod的值
limiteForPeriod50一个限制刷新期间的可用权限数
resilience4j.ratelimiter:
  configs:
    default:
      limit-for-period: 5
      limit-refresh-period: 1s
      timeout-duration: 3s
      allow-health-indicator-to-fail: true
      subscribe-for-events: true
      event-consumer-buffer-size: 50
      registerHealthIndicator: true
  instances:
    externalService:
      baseConfig: default
  1. limitForPeriod:该选项指定在特定时间段内允许的最大请求数。例如,如果将其设置为 5,则表示该时间段内仅允许 5个请求。
  2. limitRefreshPeriod:此选项定义上述时间段的持续时间。它决定重置速率限制的频率。例如,如果将其设置为 1 秒,则每秒都会重置速率限制,再次允许指定数量的请求。
  3. timeoutDuration:该选项设置请求的超时时间。如果请求超过此持续时间,则会被视为花费时间过长,并可能会触发回退行为或异常。设置为 3s 意味着如果没有机会执行,请求将在三秒后失败。
  4. registerHealthIndicator:设置为 时true,此选项启用速率限制器的运行状况指示器注册。运行状况指示器提供有关速率限制器当前状态的信息,使您可以在应用程序中监控其运行状况。
  5. eventConsumerBufferSize:此选项确定用于存储速率限制器事件的缓冲区的大小。速率限制器事件包括成功或拒绝的请求等信息。缓冲区大小应足够大,以避免在高流量期间丢失事件。

事件监听

同重试器

rateLimiter.getEventPublisher()
                .onSuccess(event -> log.info(event.toString()))
                .onFailure(event -> log.info(event.toString()));

http://localhost:8080/actuator/ratelimiterevents

指标监控

编程式获取

      RateLimiter.Metrics metrics = rateLimiter.getMetrics();
        // Returns the number of availablePermissions in this duration.
        int availablePermissions =  metrics.getAvailablePermissions();
        // Returns the number of WaitingThreads
        int numberOfWaitingThreads = metrics.getNumberOfWaitingThreads();

        log.info(time  + ", metrics[ availablePermissions=" + availablePermissions +
                ", numberOfWaitingThreads=" + numberOfWaitingThreads + " ]");

执行器端点提供有价值的信息和指标,可以帮助您监控和分析 Resilience4j 速率限制器的行为和性能。

  1. /actuator/health Resilience4j 中的端点提供有关应用程序中速率限制器的运行状况的信息。此端点概述了所有注册的速率限制器的运行状况,指示它们是否正常运行或遇到任何问题。
  2. **/actuator/ratelimiterevents:**该端点提供有关速率限制器事件的信息。它可以深入了解事件的发生情况,例如成功的请求、拒绝的请求或其他与速率限制器相关的事件。监视这些事件可以帮助您了解速率限制器的行为和性能。
  3. **/actuator/ratelimiters:**此端点提供有关应用程序中注册的速率限制器的详细信息。它提供诸如速率限制器名称之类的信息。
  4. **/actuator/metrics/resilience4j.ratelimiter.available.permissions:**此端点提供与速率限制器中的可用权限相关的指标。它提供了有关可用于在定义的速率限制内接受新请求的剩余权限或插槽数量的见解。
  5. **/actuator/metrics/resilience4j.ratelimiter.waiting_threads:**此端点提供与速率限制器中等待线程相关的指标。它提供有关当前因达到速率限制而等待从速率限制器获取权限或插槽的线程数量的信息。

为了在执行器端点中正确显示速率限制器运行状况和事件信息,请考虑在文件中添加以下属性application.yml

management:
  endpoint:
    health:
      show-details: ALWAYS
  health:
    ratelimiters:
      enabled: true
  endpoints:
    web:
      exposure:
        include: "*"

resilience4j.ratelimiter:
  instances:
    simpleRateLimit:
      limitForPeriod: 2
      limitRefreshPeriod: 15s
      timeoutDuration: 5s
      registerHealthIndicator: true
      subscribeForEvents: true

动态修改配置

可以通过动态修改属性来更新 Resilience4j 中现有实例的配置。

@Autowired
private RateLimiterRegistry rateLimiterRegistry;

......

void updateRateLimiterConfiguration(String name, int limitForPeriod, Duration timeoutDuration) {
    RateLimiter limiter = rateLimiterRegistry.rateLimiter(name);
    limiter.changeLimitForPeriod(limitForPeriod);
    limiter.changeTimeoutDuration(timeoutDuration);
}

自定义方法updateRateLimiterConfiguration允许您更改速率限制器的周期限制并调整超时持续时间。通过提供速率限制器的名称以及所需的新限制和超时值,您可以轻松调整速率限制行为,以更好地满足您的应用程序的需求。这种灵活性使您能够实时微调速率限制器,确保高效的请求处理和最佳性能。

断路器

如果微服务“A”依赖于微服务“B”。由于某种原因,微服务“B”遇到错误。微服务“A”不应重复调用微服务“B”,而应暂停(不调用),直到微服务“B”完全或部分恢复。使用**断路器,**我们可以消除故障流向下游/上游。

关闭

当一个微服务不断调用依赖的微服务时,我们称该电路处于关闭状态。

打开

当微服务不调用依赖的微服务时,它会调用为容忍故障而实现的回退方法。我们称这种状态为开放状态。当一定比例的请求失败时,比如说 90%,我们会将状态从 Closed 更改为 Open。

半开

当微服务将一定比例的请求发送到依赖的微服务,并将其余请求发送到 Fallback 方法时。我们称这种状态为半开。

在打开状态下,我们可以配置等待时长。一旦等待时间结束,断路器将进入半开状态。在此状态下,断路器会检查相关服务是否已启动。为了实现这一点,它会向我们可以配置的依赖服务发送一定比例的请求。如果它得到依赖服务的肯定响应,它将切换到关闭状态,否则它将再次返回到打开状态。

断路器可以是基于计数的或基于时间的。如果最后 N 次调用失败或速度缓慢,基于计数的断路器会将状态从关闭切换为打开。如果最后 N 秒内的响应失败或缓慢,基于时间的断路器将切换到断开状态。在这两个断路器中,我们还可以指定失败或缓慢调用的阈值。

例如,我们可以配置一个基于计数的断路器,如果最后 25 个调用中有 70% 失败或花费超过 2 秒才能完成,则“断开电路”。同样,如果过去 30 秒内 80% 的调用失败或耗时超过 5 秒,我们可以告诉基于时间的断路器断开电路。

注解式

@Service
@Slf4j
public class UserService {
    // 制定策略 和 降级方法
    @SneakyThrows
    @CircuitBreaker(name = "getUserInfo", fallbackMethod = "getUserInfoFallback")
    public String getUserInfoById(String id) {
        log.info("开始请求远程服务");
        log.info("结束请求远程服务");
        return id;
    }

    // 降级方法
    private String getUserInfoFallback(String id, Exception exception) {
         if (exception instanceof RequestNotPermitted){
            log.error("启用降级方法,id:{}", id, exception);
        }
        return "getUserInfoFallbackFromLocalCache";
    }
}

配置

resilience4j.circuitbreaker:
  configs:
    default:
      registerHealthIndicator: true
      slidingWindowSize: 10
      slidingWindowType: COUNT_BASED
      minimumNumberOfCalls: 5
      permittedNumberOfCallsInHalfOpenState: 4
      automaticTransitionFromOpenToHalfOpenEnabled: true
      waitDurationInOpenState: 5s
      failureRateThreshold: 80
      eventConsumerBufferSize: 50
  instances:
    externalService:
      baseConfig: default
  1. failure-rate-threshold=80表示如果 80% 的请求失败,则断开电路,即。使断路器状态为“打开”。
  2. sliding-window-size=10表示如果 10 个请求中有 80%(即 8 个)失败,则断开电路。
  3. sliding-window-type=COUNT_BASED表示我们正在使用 COUNT_BASED 滑动窗口。另一种类型是TIME_BASED。
  4. minimum-number-of-calls=5表示我们至少需要5次调用来计算失败率阈值。
  5. automatic-transition-from-open-to-half-open-enabled=true表示不要直接从打开状态切换到关闭状态,也考虑半开状态。
  6. permissed-number-of-calls-in-half-open-state=4表示处于half-open状态时,考虑发送4个请求。如果其中 80% 出现故障,请将断路器切换至断开状态。
  7. wait-duration-in-open-state=5s表示从打开状态切换到关闭状态时的等待时间间隔。

配置

配置参数默认值描述
failureRateThreshold50熔断器关闭状态和半开状态使用的同一个失败率阈值
ringBufferSizeInHalfOpenState10熔断器半开状态的缓冲区大小,会限制线程的并发量,例如缓冲区为10则每次只会允许10个请求调用后端服务
ringBufferSizeInClosedState100熔断器关闭状态的缓冲区大小,不会限制线程的并发量,在熔断器发生状态转换前所有请求都会调用后端服务
waitDurationInOpenState60(s)熔断器从打开状态转变为半开状态等待的时间
automaticTransitionFromOpenToHalfOpenEnabledfalse如果置为true,当等待时间结束会自动由打开变为半开,若置为false,则需要一个请求进入来触发熔断器状态转换
recordExceptionsempty需要记录为失败的异常列表
ignoreExceptionsempty需要忽略的异常列表
recordFailurethrowable -> true自定义的谓词逻辑用于判断异常是否需要记录或者需要忽略,默认所有异常都进行记录
resilience4j:
  circuitbreaker:
    configs:
      default:
        ringBufferSizeInClosedState: 5 # 熔断器关闭时的缓冲区大小
        ringBufferSizeInHalfOpenState: 2 # 熔断器半开时的缓冲区大小
        waitDurationInOpenState: 10000 # 熔断器从打开到半开需要的时间
        failureRateThreshold: 60 # 熔断器打开的失败阈值
        eventConsumerBufferSize: 10 # 事件缓冲区大小
        registerHealthIndicator: true # 健康监测
        automaticTransitionFromOpenToHalfOpenEnabled: false # 是否自动从打开到半开,不需要触发
        recordFailurePredicate:    com.example.resilience4j.exceptions.RecordFailurePredicate # 谓词设置异常是否为失败
        recordExceptions: # 记录的异常
          - com.example.resilience4j.exceptions.BusinessBException
          - com.example.resilience4j.exceptions.BusinessAException
        ignoreExceptions: # 忽略的异常
          - com.example.resilience4j.exceptions.BusinessAException
    instances:
      backendA:
        baseConfig: default
        waitDurationInOpenState: 5000
        failureRateThreshold: 20
      backendB:
        baseConfig: default

可以配置多个熔断器实例,使用不同配置或者覆盖配置。

舱壁

Bulkhead 是另一个有趣且重要的容错模式,因 Michael T Nygard 在他的书《Release It》中而闻名。在船舶中,舱壁是隔板,如果密封,可形成单独的水密隔间。如果船舶的某一部分发生损坏或漏水,舱壁会阻止水到达其他舱室,最终限制船舶的损坏并防止其下沉。

img

隔板提供的隔离同样可以应用于软件。在此模式中,依赖项是隔离的,这样一个依赖项中的资源限制不会影响其他依赖项或整个系统。这种隔离可以通过为每个依赖项分配一个线程池来实现。

img

这里我们分配了一个由 10 个线程组成的池来调用 Service1,5 个线程来调用 Service2,4 个线程来调用 Service3。它提供了两个重要的好处。

  1. 如果依赖线程花费更多时间来响应,主请求线程可能会离开。如果没有,我们将耗尽请求线程。
  2. 我们可以控制有多少并发线程调用服务,从而通过限制请求来保护服务的使用者和提供者。

它还提供了一种使用信号量控制并发线程计数的方法。因此,resilience4j 提供了带有线程池和信号量的舱壁模式。

注1:基于信号量的舱壁将使用相同的用户请求线程,并且不会创建新线程。而线程池块头会创建新的线程进行处理。

注2: ThreadPool Bulkhead仅适用于Completable Future。

注 3: Semaphore Bulkhead 是默认设置。

注解式

@Bulkhead(name="my-service1", type=Bulkhead.Type.THREADPOOL, fallbackMethod = "fallback")
public CompletableFuture<String> callService() {
     return CompletableFuture.completedFuture(sampleFeignClient.callHelpdesk());
}

@Bulkhead(name="my-service2", fallbackMethod = "fallback")
public String callService2() {
   return sampleFeignClient.callHelpdesk();
}

配置

#Threadpool bulkhead
resilience4j.thread-pool-bulkhead:
  instances:
    my-service1:
      maxThreadPoolSize: 3
      coreThreadPoolSize: 2
      queueCapacity: 1
      keepAliveDuration: 20ms 


#Semaphore bulkhead
resilience4j.bulkhead:
  instances:
    my-service2:
      maxConcurrentCalls: 2  #Max Amount of parallel execution allowed by bulkhead.
      maxWaitDuration: 5s

时间限制器

时间限制是为微服务响应设置时间限制的过程。假设微服务“A”向微服务“B”发送请求,它为微服务“B”设置响应时间限制。如果微服务“B”在该时限内没有响应,则将被认为存在故障。

Hystrix不同,Resilience4j将超时控制器从熔断器中独立出来,成为了一个单独的组件,主要的作用就是对方法调用进行超时控制。实现的原理和Hystrix相似,都是通过调用Futureget方法来进行超时控制。

Timelimiter 方面仅适用于反应式方法或 Completable future。返回的方法CompletableFuture也应该在线程池中运行。

@TimeLimiter(name="service1-tl")
public CompletableFuture<String> callService() {
     return CompletableFuture.completedFuture(sampleFeignClient.callHelpdesk());
}

配置

resilience4j.timelimiter:
  instances:
    service1-tl:
      # 超时时长
      timeoutDuration: 2s
      # 发生异常是否关闭线程
      cancelRunningFuture: true

多组件配合使用

回退方法机制的工作原理类似于 try/catch 块。如果配置了后备方法,则每个执行都会转发到后备方法执行器。回退方法执行器正在搜索可以处理异常的最佳匹配回退方法。类似于 catch 块。回退的执行与断路器的当前状态无关。

后备方法应放置在同一个类中,并且必须具有相同的方法签名,只有一个额外的目标异常参数。

如果有多个fallbackMethod方法,则将调用最接近匹配的方法,例如:

如果您尝试从 恢复,则会调用带签名的方法。NumberFormatException``String fallback(String parameter, NumberFormatException exception)}

仅当多个方法具有相同的返回类型并且您希望为它们一劳永逸地定义相同的回退方法时,您才可以定义一个带有异常参数的全局回退方法。

@CircuitBreaker(name = BACKEND, fallbackMethod = "fallback")
@RateLimiter(name = BACKEND)
@Bulkhead(name = BACKEND, fallbackMethod = "fallback")
@Retry(name = BACKEND)
@TimeLimiter(name = BACKEND)
public Mono<String> method(String param1) {
    return Mono.error(new NumberFormatException());
}

private Mono<String> fallback(String param1, CallNotPermittedException e) {
    return Mono.just("Handled the exception when the CircuitBreaker is open");
}

private Mono<String> fallback(String param1, BulkheadFullException e) {
    return Mono.just("Handled the exception when the Bulkhead is full");
}

private Mono<String> fallback(String param1, NumberFormatException e) {
    return Mono.just("Handled the NumberFormatException");
}

private Mono<String> fallback(String param1, Exception e) {
    return Mono.just("Handled any other exception");
}

resilience4j默认的Aspect顺序是

Retry ( CircuitBreaker ( RateLimiter ( TimeLimiter ( Bulkhead ( Function ) ) ) ) )

首先Bulkhead 创建一个线程池。那么TimeLimiter就可以限制线程的时间。

RateLimiter 限制可配置时间窗口内该函数的调用次数。

TimeLimiter 或 RateLimiter 抛出的任何异常都将被 CircuitBreaker 记录。然后会执行重试。

这个默认是有道理的。但出于任何原因,如果我们想要更改方面的顺序,我们可以在 application.yml 中指定,如下所示。数字越大优先级越高。根据下面的配置,重试将在断路器之前完成。

resilience4j:
  circuitbreaker:
    circuitBreakerAspectOrder: 1
  retry:
    retryAspectOrder: 2  

- resilience4j.retry.retryAspectOrder
- resilience4j.circuitbreaker.circuitBreakerAspectOrder
- resilience4j.ratelimiter.rateLimiterAspectOrder
- resilience4j.timelimiter.timeLimiterAspectOrder
- resilience4j.bulkhead.bulkheadAspectOrder

最佳实践

断路器 重试 限流器 时间限制器 舱壁,这几个组件一般的顺序是什么?

在一般情况下,将断路器、重试、限流器、时间限制器和舱壁这几个组件结合在一起时,可以考虑以下顺序作为一个基本的指导:

重试(Retry):
将重试作为第一个组件是有意义的,因为它可以尝试在短时间内恢复调用,特别是在短暂的故障情况下。设置适当的重试次数、重试间隔和退避策略,以及合理的超时设置,确保调用能够在失败后快速重试,而不会阻塞太长时间。

断路器(Circuit Breaker):
在重试尝试失败后,断路器是下一个应该考虑的组件。如果一个调用经过多次重试仍然失败,那么很可能后端服务出现了持续性的问题。在这种情况下,打开断路器可以避免频繁调用失败,节省资源,同时可以在一段时间后尝试重启。

限流器(Rate Limiter):
在重试和断路器之后,限流器是可以添加的另一个保护层。它可以控制请求的速率,防止大量的请求涌入后端服务。通过限制请求速率,可以减轻服务的负载,防止因为过多的请求导致服务不稳定。

时间限制器(Timeout):
时间限制器可以放在限流器之后,确保请求不会耗费过长时间。设置适当的超时时间,以避免因为某个请求占用资源太长时间而影响其他请求的处理。

舱壁(Bulkhead):
舱壁通常作为最后一个组件考虑。它可以帮助限制并发请求数量,确保资源在不同任务之间得到合理分配。舱壁可以防止一个失败的请求影响到其他请求的执行,从而提高系统的稳定性。

需要强调的是,这只是一种一般的顺序建议。在实际应用中,您可能需要根据系统的需求和性能目标进行调整。同时,也可以根据实际情况将不同组件灵活组合,以构建适合您应用的弹性和容错机制。最重要的是通过测试和监控,确保您的系统在各种异常情况下都能够保持稳定和可用。

配置

您可以在 Spring Boot 的配置文件中配置 CircuitBreaker、Retry、RateLimiter、Bulkhead、Thread pool Bulkhead 和 TimeLimiter 实例。 例如application.yml

resilience4j.circuitbreaker:
    instances:
        backendA:
            registerHealthIndicator: true
            slidingWindowSize: 100
        backendB:
            registerHealthIndicator: true
            slidingWindowSize: 10
            permittedNumberOfCallsInHalfOpenState: 3
            slidingWindowType: TIME_BASED
            minimumNumberOfCalls: 20
            waitDurationInOpenState: 50s
            failureRateThreshold: 50
            eventConsumerBufferSize: 10
            recordFailurePredicate: io.github.robwin.exception.RecordFailurePredicate
            
resilience4j.retry:
    instances:
        backendA:
            maxAttempts: 3
            waitDuration: 10s
            enableExponentialBackoff: true
            exponentialBackoffMultiplier: 2
            retryExceptions:
                - org.springframework.web.client.HttpServerErrorException
                - java.io.IOException
            ignoreExceptions:
                - io.github.robwin.exception.BusinessException
        backendB:
            maxAttempts: 3
            waitDuration: 10s
            retryExceptions:
                - org.springframework.web.client.HttpServerErrorException
                - java.io.IOException
            ignoreExceptions:
                - io.github.robwin.exception.BusinessException
                
resilience4j.bulkhead:
    instances:
        backendA:
            maxConcurrentCalls: 10
        backendB:
            maxWaitDuration: 10ms
            maxConcurrentCalls: 20
            
resilience4j.thread-pool-bulkhead:
  instances:
    backendC:
      maxThreadPoolSize: 1
      coreThreadPoolSize: 1
      queueCapacity: 1
      writableStackTraceEnabled: true
        
resilience4j.ratelimiter:
    instances:
        backendA:
            limitForPeriod: 10
            limitRefreshPeriod: 1s
            timeoutDuration: 0
            registerHealthIndicator: true
            eventConsumerBufferSize: 100
        backendB:
            limitForPeriod: 6
            limitRefreshPeriod: 500ms
            timeoutDuration: 3s
            
resilience4j.timelimiter:
    instances:
        backendA:
            timeoutDuration: 2s
            cancelRunningFuture: true
        backendB:
            timeoutDuration: 1s
            cancelRunningFuture: false

您还可以覆盖默认配置、定义共享配置并在 Spring Boot 的配置文件中覆盖它们。 例如:application.yml

resilience4j.circuitbreaker:
    configs:
        default:
            slidingWindowSize: 100
            permittedNumberOfCallsInHalfOpenState: 10
            waitDurationInOpenState: 10000
            failureRateThreshold: 60
            eventConsumerBufferSize: 10
            registerHealthIndicator: true
        someShared:
            slidingWindowSize: 50
            permittedNumberOfCallsInHalfOpenState: 10
    instances:
        backendA:
            baseConfig: default
            waitDurationInOpenState: 5000
        backendB:
            baseConfig: someShared

您还可以使用特定实例名称的定制器来覆盖特定 CircuitBreaker、Bulkhead、Retry、RateLimiter 或 TimeLimiter 实例的配置。下面显示了如何在 YAML 文件中覆盖上面配置的 CircuitBreaker backendA的示例:

@Bean
public CircuitBreakerConfigCustomizer testCustomizer() {
    return CircuitBreakerConfigCustomizer
        .of("backendA", builder -> builder.slidingWindowSize(100));
}
Resilienc4j TypeInstance Customizer class
Circuit breaker 断路器CircuitBreakerConfigCustomizer
Retry 重试RetryConfigCustomizer
Rate limiter 速率限制器RateLimiterConfigCustomizer
Bulkhead 舱壁BulkheadConfigCustomizer
ThreadPoolBulkhead 线程池隔板ThreadPoolBulkheadConfigCustomizer
Time Limiter 时间限制器TimeLimiterConfigCustomizer

参考:

  • https://bootcamptoprod.com/resilience4j-rate-limiter/
  • https://resilience4j.readme.io/v1.7.0/docs/getting-started-3

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/858085.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

力扣初级算法(旋转矩阵)

力扣初级算法(旋转矩阵) 每日一算法&#xff1a;旋转矩阵 学习内容&#xff1a; 1.问题&#xff1a; 给你一幅由 N N 矩阵表示的图像&#xff0c;其中每个像素的大小为 4 字节。请你设计一种算法&#xff0c;将图像旋转 90 度。 不占用额外内存空间能否做到&#xff1f; 2.…

React源码解析18(3)------ beginWork的工作流程【mount】

摘要 OK&#xff0c;经过上一篇文章。我们调用了&#xff1a; const root document.querySelector(#root); ReactDOM.createRoot(root)生成了FilberRootNode和HostRootFilber。 并且二者之间的对应关系也已经确定。 而下一步我们就需要调用render方法来讲react元素挂载在ro…

途乐证券-保险消费者信心延续恢复态势 健康险、意外险增购意愿上

今年以来&#xff0c;稳妥顾客决心延续调整康复态势。我国稳妥保证基金有限责任公司&#xff08;以下简称“稳妥保证基金公司”&#xff09;近来发布数据显现&#xff0c;2023年二季度&#xff0c;我国稳妥顾客决心指数为67.7&#xff0c;环比下降3.6&#xff0c;同比上升1.2&a…

一文读懂c++语言

一文读懂C语言 C的发展C的设计目标C的特性C的挑战 C的发展 C是一种通用的、高级的编程语言&#xff0c;它是C语言的扩展。C由Bjarne Stroustrup于1983年首次引入&#xff0c;并在之后的几十年中不断发展壮大。C被广泛应用于各种领域&#xff0c;包括系统开发、游戏开发、嵌入式…

keil显示中文代码正常,但是编译中文乱码的问题

Configuration---Editor---Encoding&#xff0c;ANSI或是UTF8&#xff0c;总之这2个&#xff0c;是A就改为U&#xff0c;是U就改为A。 MDK5中文编译乱码&#xff0c;不是显示乱码哦。

Android Camera预览画面变形问题

csdn 问题 安卓camera1在预览时&#xff0c;预览画面看起来被拉伸了&#xff0e; 如图&#xff0c;圆形的盖子&#xff0c;变成椭圆形了&#xff0e; 代码 默认流程&#xff0c;如下为大致的打开摄像头并进行预览显示的代码 private Camera mCamera null; private Surfa…

YOLOv5入门

模型检测 关键参数 weights:训练好的模型文件 source: 检测的目标&#xff0c;可以是单张图片、文件夹、屏幕或者摄像头等 conf-thres: 置信度闯值&#xff0c;越低框越多&#xff0c;越高框越少 iou-thres: IOU闻值&#xff0c;越低框越少&#xff0c;越少框越多 torch.hu…

windows安装apache-jmeter-5.6.2教程

目录 一、下载安装包&#xff08;推荐第二种&#xff09; 二、安装jmeter 三、启动jmeter 一、下载安装包&#xff08;推荐第二种&#xff09; 1.官网下载&#xff1a;Apache JMeter - Download Apache JMeter 2.百度云下载&#xff1a;链接&#xff1a;https://pan.baidu.…

vivo 场景下的 H5无障碍适配实践

作者&#xff1a;vivo 互联网前端团队- Zhang Li、Dai Wenkuan 随着信息无障碍的建设越来越受重视&#xff0c;开发人员在无障碍适配中也遇到了越来越多的挑战。本文是笔者在vivo开发H5项目做无障碍适配的实践总结。本文主要介绍了在前端项目中常用的无障碍手势和无障碍属性&am…

kubeadm安装

master&#xff08;2C/4G&#xff0c;cpu核心数要求大于2&#xff09; 192.168.223.71 node01&#xff08;2C/2G&#xff09; 192.168.223.72 node02&#xff08;2C/2G&#xff09; 192.168.223.73…

validator入门

validator中文文档地址和英文地址 https://docs.jboss.org/hibernate/validator/4.2/reference/zh-CN/html/validator-gettingstarted.html https://docs.jboss.org/hibernate/validator/6.0/reference/en-US/html_single/#preface自定义hibernate-validator校验 工具类Valid…

基于SOM神经网络的柴油机故障诊断

1.案例背景 1.1 SOM神经网络概述 自组织特征映射网络(Self - Organizing Feature Map,SOM)也称Kohonen网络,它是由荷兰学者Teuvo Kohonen于1981年提出的。该网络是一个由全连接的神经元阵列组成的无教师、自组织、自学习网络。Kohonen认为,处于空间中不同区域的神经元有着不同…

别克VELITE 5增程式混动电动汽车变速箱解析

别克VELITE 5增程式电动汽车智能无极变速箱采用双电机双行星轮组的设计&#xff0c;相比其他技术具有更高的自由度&#xff0c;两个点击可同时参与驱动或单独发电&#xff0c;在内部集成的TPIM 模块的控制下&#xff0c;发动机与点击可同时100%参与驱动&#xff0c;变速范围更加…

嘉楠勘智k230开发板上手记录(二)--hello world

上次成功在k230上烧录sdk&#xff0c;这次准备实现hello world和ssh scp远程k230 主要是按照K230 SDK 基础教程的K230_实战基础篇_hello_world.md 一、PC连接k230 1. 初步准备 首先下载串口工具PuTTY&#xff0c;这个我个人感觉比较方便。 准备两根USB type-C数据线&#…

Vue + MapBox快速搭建

一、说明&#xff1a; 1.mapbox-gl自2.0版本开始不再开源&#xff0c;需要用户在官网申请key使用。 2.maplibre GL JS是一个开源库&#xff0c;它起源于 mapbox-gl-js 的开源分支。该库的初始版本&#xff08;1.x&#xff09;旨在替代Mapbox的OSS版本。简单来说maplibre是mapb…

老师如何设计一个实用的分班查询系统?

暑期过后&#xff0c;学校将迎来分班工作。有些是小升初需要分班&#xff0c;有些是高一升高二需要分班。对于老师来说&#xff0c;直接将分班结果发送到班级群&#xff0c;家长找不到结果时会发送信息询问&#xff0c;放假期间老师也需要时刻盯着手机。 不过&#xff0c;聪明…

公网环境Windows系统,远程桌面控制树莓派《内网穿透》

远程桌面控制树莓派&#xff0c;我们可以用xrdp协议来实现&#xff0c;它内部使用的是windows远程桌面的协议。我们只需要在树莓派上安装xrdp&#xff0c;就可以在同个局域网下远程桌面控制树莓派。 而如果需要在公网下远程桌面控制树莓派&#xff0c;可以通过cpolar内网穿透&a…

C语言笔试训练【第七天】

文章目录 &#x1fa92;1、以下对C语言函数的有关描述中&#xff0c;正确的有【多选】&#xff08; &#xff09;&#x1f9ef;2、在C语言中&#xff0c;以下正确的说法是&#xff08; &#xff09;&#x1faaa;3、在上下文及头文件均正常的情况下&#xff0c;下列代码的输出是…

跨境干货|TikTok变现的9种方法

在这个流量为王的时代&#xff0c;哪里有流量&#xff0c;哪里就有商机。TikTok作为近几年最火爆的社媒平台之一&#xff0c;在全球范围都具有一定的影响力。随着TikTok Shop等商务功能加持上线&#xff0c;更是称为跨境电商的新主场之一。 在这样的UGC平台&#xff0c;想要变…

VM官网下载VM workstations pro

官网下载https://www.vmware.com/products.html 此处需要注册账号下载&#xff0c;用邮箱注册一个一账号就可以了&#xff0c;下载就完成了