resilience4j在业务系统中的实践(Trae小试牛刀)

AI 时代程序员必备技能

Codex、Claude Code、Cursor、Hermes Agent、OpenClaw等工程化实战专栏 ,讲透 AI 如何接管脏活累活

虽然现在AI已经那么强了,想了下,还是想分享一下,特别是有一些权衡点,还是值得思考的。

这里我使用trae AI IDE工具进行测试验证,充分发挥AI的能力。

第一句:制定计划

我:帮我在这个目录下,建立一个测试springboot2下的resilience4j使用的示例工程,先不要动手写代码,先做大的方案,再做计划,最后再开发。

输出信息

1.技术栈:

2.项目结构:

hello-resilience4j-springboot2/
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/com/example/resilience4j/
│   │   │   ├── Resilience4jApplication.java       # 启动类
│   │   │   ├── controller/
│   │   │   │   └── DemoController.java            # REST API 入口
│   │   │   ├── service/
│   │   │   │   └── DemoService.java               # 业务逻辑(模拟各种场景)
│   │   │   └── config/
│   │   │       └── Resilience4jConfig.java        # 可选的 Java 配置
│   │   └── resources/
│   │       └── application.yml                    # Resilience4j 配置
│   └── test/
│       └── ...                                    # 测试类
└── README.md

3.功能模块设计

限流、熔断、重试、舱壁隔离、超时、组合使用。。。(细节省略)

第二句:缩小范围

我:我只测试熔断,不测试其他的

输出:好的,简化方案,只聚焦 熔断器(CircuitBreaker) 。(细节省略)确认无误后我就开始创建。

第三句:确认执行

我:确认

输出:所有文件已创建完成。你可以在本地终端执行以下命令来编译和运行:

我:给application.yml的配置项加上注释,加到配置项后面,而不是上面

输出:已完成。所有配置项都在行末添加了中文注释,说明了每个参数的作用。

第四句:解释代码

我:解释一下, CircuitBreakerService.java 32-35 fallbackMethod="fallbackFail"作用是啥

输出:fallbackMethod = "fallbackFail" 的作用是 指定熔断器的降级方法 。(细节省略)

部分代码展示


@Service
public class CircuitBreakerService {

    private final AtomicLong callCounter = new AtomicLong(0);

    /**
     * 受熔断器保护的正常服务调用
     */
    @CircuitBreaker(name = "backendA", fallbackMethod = "fallbackGetData")
    public String getData() {
        callCounter.incrementAndGet();
        return "Success response from backend, call #" + callCounter.get();
    }

    /**
     * 熔断器打开时的降级方法
     */
    public String fallbackGetData(Throwable t) {
        return "Fallback: backend service is unavailable, circuit breaker is OPEN. Reason: " + t.getMessage();
    }

    /**
     * 模拟失败的服务调用(用于触发熔断)
     */
    @CircuitBreaker(name = "backendA", fallbackMethod = "fallbackFail")
    public String fail() {
        throw new RuntimeException("Simulated backend failure");
    }

    /**
     * fail() 方法的降级
     */
    public String fallbackFail(Throwable t) {
        return "Fallback: fail() triggered - " + t.getMessage();
    }
}

application.yml

server:
  port: 8080 # 服务端口

resilience4j:
  circuitbreaker: # 熔断器配置
    instances: # 实例配置
      backendA: # 名为backendA的实例
        failureRateThreshold: 50 # 失败率阈值(百分比),超过此值触发熔断
        waitDurationInOpenState: 10s # 熔断器打开状态下等待多久后进入半开状态
        slidingWindowSize: 10 # 滑动窗口大小
        minimumNumberOfCalls: 5 # 计算失败率所需的最小调用次数
        permittedNumberOfCallsInHalfOpenState: 3 # 半开状态下允许的最大调用次数
        slidingWindowType: COUNT_BASED # 滑动窗口类型(基于调用次数)

@RestController
@RequestMapping("/api")
public class CircuitBreakerController {

    private final CircuitBreakerService circuitBreakerService;
    private final CircuitBreakerRegistry circuitBreakerRegistry;

    public CircuitBreakerController(CircuitBreakerService circuitBreakerService,
                                    CircuitBreakerRegistry circuitBreakerRegistry) {
        this.circuitBreakerService = circuitBreakerService;
        this.circuitBreakerRegistry = circuitBreakerRegistry;
    }

    /**
     * 正常调用(受熔断器保护)
     */
    @GetMapping("/data")
    public String getData() {
        return circuitBreakerService.getData();
    }

    /**
     * 模拟失败调用(用于触发熔断)
     */
    @GetMapping("/fail")
    public String fail() {
        return circuitBreakerService.fail();
    }

    /**
     * 查看当前熔断器状态
     */
    @GetMapping("/state")
    public String getState() {
        io.github.resilience4j.circuitbreaker.CircuitBreaker cb =
                circuitBreakerRegistry.circuitBreaker("backendA");
        return "CircuitBreaker State: " + cb.getState() +
                "\nMetrics: " + cb.getMetrics();
    }
}

启动测试

安装Trae提示的java插件、maven插件等,点击三角符号旁边的下拉启动debug调试。

启动日志在下方控制台区域

测试方法:

浏览器访问地址http://localhost:8080/api/data多次,输出正常。

浏览器访问地址http://localhost:8080/api/fail多次,第四次熔断提示fallbackFail的信息。

熔断后再次访问 http://localhost:8080/api/data提示熔断,返回fallbackGetData的信息。

分析下,为什么是在第4次失败时熔断的?

failureRateThreshold: 50 失败比例达到50%就触发熔断。

slidingWindowSize: 10 窗口大小为10,就是最大统计最近10次请求

minimumNumberOfCalls: 5 统计窗口内达到5次请求,就属于有效统计窗口。

恰好,当请求次数达到6次,失败数目达到3次,即可触发熔断。

提示:与minimumNumberOfCalls的关系,窗口最大记录最近10次请求,并且,窗口内请求数低于5时,本次统计不触发熔断,等到更多的请求数过来才触发,避免抖动带来的熔断阈值过低问题。

经测试,发现符合预期。

第五句:去掉fallbackMethod

我:我感觉这个fallbackMethod有点鸡肋,我更想得到真正的错误,而不是返回status 200,当我业务调用链路很长时,这可能会导致业务未异常中断。去掉fallbackMethod

输出:已移除两个方法的 fallbackMethod 配置及对应的降级方法。现在熔断触发时会直接抛出 CallNotPermittedException ,异常会向上冒泡到 Controller,最终由GlobalExceptionHandler.handleCallNotPermitted 捕获处理,调用方可以感知到真正的错误。

重新启动服务,验证一下。(因为我想ctrl+shift+F9单独编译某个java类,我还是不习惯在trae里启动java服务,回到IDEA里启动了^_^)

访问多次 http://localhost:8080/api/api后,触发熔断

访问http://localhost:8080/api/data熔断后提示503 Service unavailable: Circuit Breaker is OPEN,CircuitBreaker 'backendA' is OPEN and does not permit further calls。

提示:如果不想对业务端暴露这么技术性强的提示,可以在异常拦截里获取CallNotPermittedException的causingCircuitBreakerName属性,也就是“backendA”,然后按需要提示)。

第六句:贴近业务场景

业务场景中很多时候是超时熔断,而不是报错而熔断。但是考虑到超时TimeoutException属于受检异常,我如果对某个方法加@TimeLimiter,那我就得把返回值改成CompletableFuture对象,业务中使用的时候就得future.get(),我觉得不太方便。所以我就在关键方法中,自己future.get()等待,同时把异常转换为非受检异常,同时把异常添加到recordExceptions配置中,这样对业务调用来说影响最小,超时时间由我自己完全控制。

我:再在public class CircuitBreakerService里添加一个方法,这个方法通过CompletableFuture调用另一个方法(可以只是个System.out.println),超时时触发TimeoutException,但是我不想修改代码throws这个异常,所以再自己封装一个MyRuntimeTimeoutException,然后配置到yml里的recordExceptions里。

我:CircuitBreakerService.java 33-53 把这个地方改成同步读取future.get,返回的不是future

手动调整了一下,模拟耗时500ms,超时等待300ms,造一个超时异常。

CircuitBreakerService.java

/**
     * 通过 CompletableFuture 异步调用,同步等待结果,受超时和熔断保护
     */
    @CircuitBreaker(name = "backendA")
    public String callWithTimeout() {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                // 模拟耗时操作
                Thread.sleep(500);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Async task completed");
            return "Async result";
        });

        try {
            // 同步等待,超时则抛出 TimeoutException
            return future.get(300, TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
            throw new MyRuntimeTimeoutException("Timeout occurred: " + e.getMessage(), e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new MyRuntimeTimeoutException("Interrupted: " + e.getMessage(), e);
        } catch (ExecutionException e) {
            throw new MyRuntimeTimeoutException("Execution failed: " + e.getMessage(), e);
        }
    }

application.yml (关注recordExceptions配置)

resilience4j:
  circuitbreaker: # 熔断器配置
    instances: # 实例配置
      backendA: # 名为backendA的实例
        failureRateThreshold: 20 # 失败率阈值(百分比),超过此值触发熔断
        waitDurationInOpenState: 10s # 熔断器打开状态下等待多久后进入半开状态
        slidingWindowSize: 10 # 滑动窗口大小
        minimumNumberOfCalls: 2 # 计算失败率所需的最小调用次数
        permittedNumberOfCallsInHalfOpenState: 3 # 半开状态下允许的最大调用次数
        slidingWindowType: COUNT_BASED # 滑动窗口类型(基于调用次数)
        recordExceptions: # 记录为失败的异常类型
          - com.example.resilience4j.exception.MyRuntimeTimeoutException
          - java.lang.RuntimeException

CircuitBreakerController.java

    /**
     * 超时调用(受超时限制和熔断器保护)
     */
    @GetMapping("/timeout")
    public String callWithTimeout() {
        return circuitBreakerService.callWithTimeout();
    }

测试验证:

访问 http://localhost:8080/api/timeout 多次,这里我调整了配置,第三次就会熔断

继续访问 http://localhost:8080/api/data或其他接口,都提示熔断的错误。

第七句:配置优化

我:最后把yml配置优化一下,提取default配置,使用base-config扩展配置,这样多个实例可以复用配置。

输出:

application.yml

resilience4j:
  circuitbreaker: # 熔断器配置
    configs: # 共享配置模板
      default: # 默认配置,所有实例默认继承
        failureRateThreshold: 50 # 失败率阈值(百分比),超过此值触发熔断
        waitDurationInOpenState: 10s # 熔断器打开状态下等待多久后进入半开状态
        slidingWindowSize: 10 # 滑动窗口大小
        minimumNumberOfCalls: 5 # 计算失败率所需的最小调用次数
        permittedNumberOfCallsInHalfOpenState: 3 # 半开状态下允许的最大调用次数
        slidingWindowType: COUNT_BASED # 滑动窗口类型(基于调用次数)
        recordExceptions: # 记录为失败的异常类型
          - com.example.resilience4j.exception.MyRuntimeTimeoutException
          - java.lang.RuntimeException
    instances: # 实例配置
      backendA: # 名为backendA的实例
        baseConfig: default # 继承default配置
        failureRateThreshold: 20 # 失败率阈值(百分比),超过此值触发熔断
        minimumNumberOfCalls: 2 # 计算失败率所需的最小调用次数

这样后续就算配置实例2、实例3,也仅需要增加很少的配置,或什么也不加,他自动会读取default配置。

源码佐证:

CircuitBreakerConfigurationProperties.java

    public Optional<CircuitBreakerConfigurationProperties.InstanceProperties> findCircuitBreakerProperties(String name) {
        CircuitBreakerConfigurationProperties.InstanceProperties instanceProperties = (CircuitBreakerConfigurationProperties.InstanceProperties)this.instances.get(name);
        if (instanceProperties == null) {
            instanceProperties = (CircuitBreakerConfigurationProperties.InstanceProperties)this.configs.get("default");
        }

        return Optional.ofNullable(instanceProperties);
    }

第八句:超时时间也配进去

我:最后把我手动在 `e:\study\backend\hello-resilience4j-springboot2\src\main\java\com\example\resilience4j\service\CircuitBreakerService.java#L51-54` 指定的超时时间也配置到yml里去,这里使用@Value注入读取超时时间。key为myself-custom-timeout表示是我自己加的。

我:改一下,把配置添加到resilience4j-extra.instances.backendA下面。

我:你再改一下CircuitBreakerService,默认取实例配置,取不到就取default配置。

我:把yml里的 application.yml 19-24 的timeout改成duration类型,CircuitBreakerService注入为Duration实例,让配置更优雅。

输出:

application.yml

resilience4j-extra:
  instances:
    default:
      timeout: 300ms # 自定义超时时间
    backendA:
      timeout: 300ms # 自定义超时时间

CircuitBreakerService.java

    @Value("${resilience4j-extra.instances.backendA.timeout:${resilience4j-extra.instances.default.timeout}}")
    private Duration timeout;


    @CircuitBreaker(name = "backendA")
    public String callWithTimeout() {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                // 模拟耗时操作
                Thread.sleep(500);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Async task completed");
            return "Async result";
        });

        try {
            // 同步等待,超时则抛出 TimeoutException
            return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
        }
        // ... 省略
    }

完美收官

    本来想自己从头写个示例,但是自从有了AI,越来越懒了,以前写这一篇大概需要断断续续一整天时间,本次总体耗时半天左右,主要是不用一点点敲代码了,整体上轻松了不少。我本次使用的qwen3.6-plus模型,整体体验不错。建议:会话很长的时候,新开会话,节省上下文,但是也要注意新开上下文的时候,注意时机,要不他会做一些额外的事情。整体耗费:0.5元左右,完全值得。

另外:可使用cc-switch,配置模型服务代理,我主要看中了他的模型计费能力,因为我发现阿里云的计费不够直观,而且是后付费模式。

cc-switch截图:

这里计费为0.4,因为我昨天晚上也使用了,他只就算了当天的。

豆包配置:

15721是cc-switch的代理端口,API密钥随便写,真实的API密钥在cc-switch里配置了。

项目很简单,我上传到gitee上了,可以参考一下。

https://gitee.com/brimsullowr/hello-resilience4j-springboot2

AI 时代程序员必备技能

Codex、Claude Code、Cursor、Hermes Agent、OpenClaw等工程化实战专栏 ,讲透 AI 如何接管脏活累活

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

brimsullowr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值