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

resilience4j在业务系统中的实践(Trae小试牛刀) 虽然现在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.md3.功能模块设计限流、熔断、重试、舱壁隔离、超时、组合使用。。。细节省略第二句缩小范围我我只测试熔断不测试其他的输出好的简化方案只聚焦 熔断器(CircuitBreaker) 。细节省略确认无误后我就开始创建。第三句确认执行我确认输出所有文件已创建完成。你可以在本地终端执行以下命令来编译和运行我给application.yml的配置项加上注释加到配置项后面而不是上面输出已完成。所有配置项都在行末添加了中文注释说明了每个参数的作用。第四句解释代码我解释一下 CircuitBreakerService.java 32-35 fallbackMethodfallbackFail作用是啥输出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.ymlserver: 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 捕获处理调用方可以感知到真正的错误。重新启动服务验证一下。因为我想ctrlshiftF9单独编译某个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() { CompletableFutureString 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.RuntimeExceptionCircuitBreakerController.java/** * 超时调用受超时限制和熔断器保护 */ GetMapping(/timeout) public String callWithTimeout() { return circuitBreakerService.callWithTimeout(); }测试验证访问 http://localhost:8080/api/timeout 多次这里我调整了配置第三次就会熔断继续访问 http://localhost:8080/api/data或其他接口都提示熔断的错误。第七句配置优化我最后把yml配置优化一下提取default配置使用base-config扩展配置这样多个实例可以复用配置。输出application.ymlresilience4j: 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.javapublic OptionalCircuitBreakerConfigurationProperties.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.ymlresilience4j-extra: instances: default: timeout: 300ms # 自定义超时时间 backendA: timeout: 300ms # 自定义超时时间CircuitBreakerService.javaValue(${resilience4j-extra.instances.backendA.timeout:${resilience4j-extra.instances.default.timeout}}) private Duration timeout; CircuitBreaker(name backendA) public String callWithTimeout() { CompletableFutureString 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