线上服务频繁超时?用 Resilience4j 打造高可用系统

0 阅读8分钟

前夜:一次线上故障引发的思考

上周五晚上,我正准备下班,突然手机疯狂震动——告警群里消息炸了:

⚠️ 订单服务响应时间超过 30s! ⚠️ 价格服务超时率 25%! ⚠️ 用户投诉无法提交订单!

紧急登录服务器查看日志,发现问题根源:供应商接口超时,导致我们的服务一直等待,线程池耗尽,整个系统雪崩。

如果当时有熔断机制,故障的影响范围完全可以控制在最小。今天就来聊聊如何在 Spring Boot 项目中用 Resilience4j 打造高可用系统。


什么是 Resilience4j?

一句话解释

Resilience4j 是一个轻量级的容错库,通过熔断、限流、重试、超时等机制,保护你的服务不被下游故障拖垮。

想象一下:你开车上班(调用下游服务),遇到堵车(服务故障):

  • 没有导航:一直在原地等待,迟到(服务雪崩)
  • 有 Resilience4j:导航提示拥堵,自动换路(熔断),准时到达(系统稳定)

核心功能模块

模块作用生活类比
CircuitBreaker熔断器家里的保险丝,过载自动跳闸
Retry重试打电话不通,再打几次
TimeLimiter超时控制等电梯超过30秒就走楼梯
Bulkhead舱壁隔离泰坦尼克号隔舱,一处漏水不影响整体
RateLimiter限流景区排队,每小时放行100人

与 Hystrix 的区别

对比项HystrixResilience4j
状态已停止维护活跃维护中 ✅
性能依赖 RxJava,较重轻量,支持多种库
学习曲线较陡平缓,文档友好
Spring Boot需要额外配置原生支持,开箱即用

💡 建议:新项目直接用 Resilience4j,老项目逐步迁移。


快速上手:5分钟集成

别被高大上的概念吓到,Resilience4j 用起来非常简单!

1. 添加依赖

pom.xml 中添加:

<dependencies>
    <!-- Resilience4j Spring Boot Starter -->
    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-spring-boot3</artifactId>
        <version>2.1.0</version>
    </dependency>

    <!-- AOP 注解支持 -->
    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-aop</artifactId>
        <version>2.1.0</version>
    </dependency>
</dependencies>

2. 写配置

application.yml 中加几行配置:

resilience4j:
  circuitbreaker:
    configs:
      default:
        sliding-window-size: 10          # 滑动窗口大小
        failure-rate-threshold: 50       # 失败率超过 50% 就熔断
        wait-duration-in-open-state: 10s # 熔断后等 10 秒再尝试
  timelimiter:
    configs:
      default:
        timeout-duration: 3s             # 超时时间 3 秒

3. 加注解

@Service
public class OrderService {

    @CircuitBreaker(name = "backendA", fallbackMethod = "fallback")
    @TimeLimiter(name = "default")
    public CompletableFuture<String> createOrder(OrderRequest request) {
        // 调用下游服务
        return CompletableFuture.supplyAsync(() ->
            remoteService.createOrder(request)
        );
    }

    // 降级方法:出问题时调用这个
    private CompletableFuture<String> fallback(OrderRequest request, Exception ex) {
        return CompletableFuture.completedFuture("订单创建失败,请稍后重试");
    }
}

搞定!就这几行代码,你的服务已经有了容错能力。


四大核心模块详解

1. CircuitBreaker - 熔断器(最重要)

熔断器是 Resilience4j 的核心,学会它就算入门了一半。

三种状态详解

想象一下你家的电闸

状态说明请求处理
CLOSED(关闭)正常供电正常处理请求,统计失败率
OPEN(打开)跳闸了直接拒绝请求,不走网络
HALF_OPEN(半开)试试有没有电允许少量请求测试服务是否恢复

状态流转图

正常状态(CLOSED)
    │
    │ 失败率超过阈值(比如 50%)
    ▼
熔断状态(OPEN)← 直接拒绝,调用降级逻辑
    │
    │ 等待时间结束(比如 10 秒)
    ▼
半开状态(HALF_OPEN)← 放行 3 个请求试试
    │
    │ 测试请求都成功
    ▼
恢复正常(CLOSED

实战配置

resilience4j:
  circuitbreaker:
    instances:
      priceService:                    # 给价格服务单独配置
        sliding-window-type: COUNT_BASED    # 基于请求数统计
        sliding-window-size: 100            # 看最近 100 个请求
        minimum-number-of-calls: 10         # 至少 10 个请求才统计
        failure-rate-threshold: 50          # 失败率 50% 触发熔断
        wait-duration-in-open-state: 60s    # 熔断 60 秒后尝试恢复
        permitted-number-of-calls-in-half-open-state: 3  # 半开状态放行 3 个

代码示例

@Service
public class PriceService {

    @CircuitBreaker(
        name = "priceService",
        fallbackMethod = "getFallbackPrice"
    )
    public BigDecimal getPrice(String productId) {
        // 调用价格接口,可能超时或报错
        return priceRemoteClient.getPrice(productId);
    }

    // 降级方法:参数要多一个 Exception
    private BigDecimal getFallbackPrice(String productId, Exception ex) {
        log.warn("价格服务熔断,使用默认价格, productId={}", productId);
        return BigDecimal.ZERO;  // 返回默认值,或者从缓存拿
    }
}

⚠️ 注意:降级方法签名必须和原方法一致,额外加一个 Exception 参数!


2. TimeLimiter - 超时控制

为什么需要超时?

没有超时控制:
用户请求 → 你的服务 → 下游服务(卡住 30 秒)
              ↑
         线程一直等待,直到耗尽

有超时控制:
用户请求 → 你的服务 → 下游服务(3 秒超时)
              ↑
         3 秒后返回,释放线程

线程是稀缺资源,等久了会拖垮整个系统!

配置示例

resilience4j:
  timelimiter:
    configs:
      default:
        timeout-duration: 3s             # 等 3 秒还没响应就算超时
        cancel-running-future: true      # 超时后取消正在执行的任务
    instances:
      slowService:
        timeout-duration: 10s            # 慢服务可以多等一会儿

使用方式

@TimeLimiter(name = "slowService")
public CompletableFuture<String> slowOperation() {
    return CompletableFuture.supplyAsync(() -> {
        Thread.sleep(5000);  // 模拟耗时操作
        return "done";
    });
}

💡 提示:TimeLimiter 只能用于返回 CompletableFuture 的方法!


3. Retry - 重试机制

什么时候该重试?

  • ✅ 网络抖动导致的临时故障
  • ✅ 服务重启导致的短暂不可用
  • ❌ 业务错误(如余额不足)
  • ❌ 超时错误(已经等很久了,重试没用)

配置示例

resilience4j:
  retry:
    configs:
      default:
        max-attempts: 3                 # 最多重试 3 次
        wait-duration: 1s               # 每次间隔 1 秒
        retry-exceptions:               # 这些异常才重试
          - java.io.IOException
          - java.net.SocketTimeoutException
        ignore-exceptions:              # 这些异常不重试
          - java.lang.IllegalArgumentException

代码示例

@Retry(name = "databaseService", fallbackMethod = "saveToCache")
public void saveData(Data data) {
    databaseRepository.save(data);  // 可能偶尔失败
}

// 重试 3 次都失败了?走这里
private void saveToCache(Data data, Exception ex) {
    // 存到缓存,稍后重试
    cacheService.save(data);
}

指数退避(高级)

每次失败后,等待时间翻倍,避免雪上加霜:

resilience4j:
  retry:
    configs:
      default:
        max-attempts: 5
        interval-function: exponential    # 指数退避
        exponential-initial-wait: 100ms  # 第一次等 100ms
        exponential-max-wait: 5s         # 最多等 5 秒
        exponential-multiplier: 2        # 每次翻倍

实际等待时间:100ms → 200ms → 400ms → 800ms → 1600ms → 5000ms


4. Bulkhead - 舱壁隔离

为什么需要隔离?

没有隔离:
线程池(10 个线程)
  ├─ 8 个线程被慢服务 A 占用
  ├─ 2 个线程可用
  └─ 服务 B、C、D 都在等线程 → 全部卡住

有隔离:
线程池 A5 个)← 服务 A 专用
线程池 B3 个)← 服务 B 专用
线程池 C(2 个)← 服务 C 专用

这就是泰坦尼克号的原理:一个舱进水了,其他舱还能漂!

两种模式

模式特点适用场景
Semaphore轻量,类似计数器大部分场景
FixedThreadPool用独立线程池需要等待控制的场景

配置示例

resilience4j:
  bulkhead:
    configs:
      default:
        max-concurrent-calls: 10        # 最多 10 个并发
    instances:
      heavyService:
        max-concurrent-calls: 5         # 重服务限制 5 个并发

实战:配置生产级超时策略

超时金字塔

记住这个原则:外层超时 > 内层超时

                    ┌─────────────────────────┐
                    │  Resilience4j           │
                    │  TimeLimiter (30s)      │  ← 最外层,兜底
                    └───────────┬─────────────┘
                                │
                    ┌───────────┴─────────────┐
                    │  Feign Client           │
                    │  read-timeout (10s)     │  ← 中层,服务间调用
                    └───────────┬─────────────┘
                                │
                    ┌───────────┴─────────────┐
                    │  供应商 HTTP             │
                    │  response-timeout (3s)  │  ← 最内层
                    └─────────────────────────┘

完整配置示例

spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            connect-timeout: 3000        # 连接超时 3 秒
            read-timeout: 10000          # 读取超时 10 秒

resilience4j:
  timelimiter:
    configs:
      default:
        timeout-duration: 15s           # 必须大于 Feign read-timeout
    instances:
      priceService:
        timeout-duration: 35s           # 价格服务可以久一点

  circuitbreaker:
    configs:
      default:
        sliding-window-size: 100
        failure-rate-threshold: 50
        wait-duration-in-open-state: 30s
    instances:
      priceService:
        sliding-window-size: 50
        failure-rate-threshold: 30       # 更敏感,30% 就熔断

避坑指南

❌ 错误 1:超时层级混乱

// 错误示例
// Feign read-timeout: 30s
// TimeLimiter: 10s
// 问题:TimeLimiter 先超时了,Feign 还没反应

正确:TimeLimiter > Feign read-timeout


❌ 错误 2:降级方法签名不对

// 错误!缺少 Exception 参数
private String fallback(String id) { ... }

// 正确
private String fallback(String id, Exception ex) { ... }

❌ 错误 3:什么接口都加熔断

// 一个简单的 hello 方法需要熔断吗?
@CircuitBreaker(name = "service")
public String hello() {
    return "Hello";
}

建议:只在调用外部服务、第三方 API 时使用。


❌ 错误 4:降级逻辑也出错

// 危险!降级方法也可能失败
private String fallback(String id, Exception ex) {
    return databaseClient.get(id);  // 又调用数据库?
}

建议:降级逻辑要简单可靠,返回默认值或从缓存拿。


❌ 错误 5:不监控

配置了熔断,但不知道:

  • 熔断器打开过几次?
  • 降级调用了多少次?
  • 哪个服务最不稳定?

建议:集成 Micrometer + Prometheus 监控

management:
  endpoints:
    web:
      exposure:
        include: '*'

总结

Resilience4j 是微服务高可用的必备神器,记住这四个核心:

  1. CircuitBreaker - 快速失败,避免雪崩
  2. TimeLimiter - 超时控制,释放资源
  3. Retry - 自动重试,提高成功率
  4. Bulkhead - 资源隔离,互不影响

核心原则

  • 配置合理的超时层级(外层 > 内层)
  • 降级逻辑要简单可靠
  • 监控告警要及时准确
  • 宁可快失败,不要慢等待