Java 服务重试机制方案详解

一、引言

1.1 为什么需要重试机制

在分布式系统和微服务架构中,服务调用失败是不可避免的。失败原因可能包括:

  • 网络不稳定:网络抖动、连接超时、服务暂时不可达
  • 资源竞争:数据库乐观锁冲突、并发限流触发
  • 外部依赖故障:第三方服务宕机、API 调用失败
  • 瞬时故障:数据库连接池耗尽、服务重启期间

重试机制的核心价值在于:将暂时性故障转化为最终成功,提升系统鲁棒性

1.2 常见应用场景

场景类型 典型示例 重试必要性
数据库操作 乐观锁冲突、死锁检测 高并发场景必然发生
外部服务调用 HTTP API、RPC 调用 网络不可靠性
消息处理 MQ 消费失败 消息不能丢失
分布式事务 跨服务协调失败 最终一致性保障
资源获取 分布式锁获取失败 竞争场景常见

二、同步重试方案

方案一:Spring Retry

概述

Spring Retry 是 Spring 官方提供的重试框架,基于 AOP 实现,通过注解声明式配置重试策略。它与 Spring 事务管理无缝集成,能够捕获事务提交时抛出的异常。

核心原理:通过代理拦截目标方法,捕获指定异常后按策略重试。

优缺点

优点 缺点
注解驱动,配置简洁 同步阻塞,占用线程资源
与 Spring 事务无缝集成 不支持复杂重试条件判断
支持多种退避策略 重试期间无法处理其他请求
Spring Boot(需显式引入) 仅适用于短暂性故障

适用场景

  • 数据库乐观锁冲突重试
  • 短暂网络故障恢复
  • 数据库连接池瞬时耗尽
  • @Transactional 配合的事务内重试

使用示例:乐观锁冲突重试

@SpringBootApplication
@EnableRetry  // 启用重试功能
public class Application { }

@Service
public class OrderService {
    
    /**
     * 乐观锁重试示例
     * maxAttempts: 最大重试次数
     * backoff: 退避策略,延迟递增
     */
    @Retryable(
        value = OptimisticLockingFailureException.class,
        maxAttempts = 3,
        backoff = @Backoff(delay = 100, multiplier = 2)
    )
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateOrderStatus(String orderId, String status) {
        Order order = orderRepository.findById(orderId);
        order.setStatus(status);
        orderRepository.save(order);  // 可能抛出乐观锁异常
    }
    
    /**
     * 重试失败后的兜底处理
     */
    @Recover
    public void recoverUpdate(OptimisticLockingFailureException e, String orderId) {
        log.error("订单状态更新失败,超过最大重试次数: {}", orderId);
        // 发送告警或记录失败日志
    }
}

关键配置说明

  • @EnableRetry:启动类启用重试功能
  • value:指定重试的异常类型
  • maxAttempts:最大重试次数(默认3次)
  • backoff:退避策略,避免立即重试加剧竞争
  • @Recover:重试耗尽后的兜底方法

@Backoff 退避策略参数详解

@Backoff 注解支持以下参数:

参数 含义 默认值 说明
delay 初始延迟时间 1000ms 第一次重试前的等待时间(毫秒)
multiplier 乘数因子 0(不递增) 每次重试后延迟时间乘以该值,实现指数退避
maxDelay 最大延迟上限 无上限 限制延迟时间的最大值,防止无限增长

延迟计算公式:第 n 次重试的延迟时间 = delay × multiplier^(n-1)

示例说明

@Backoff(delay = 100, multiplier = 2)
// 重试延迟计算:
// 第1次重试:等待 100ms    (100 × 2^0)
// 第2次重试:等待 200ms    (100 × 2^1)
// 第3次重试:等待 400ms    (100 × 2^2)

@Backoff(delay = 500, multiplier = 1.5, maxDelay = 5000)
// 重试延迟计算:
// 第1次重试:等待 500ms
// 第2次重试:等待 750ms    (500 × 1.5)
// 第3次重试:等待 1125ms   (750 × 1.5)
// 第4次重试:等待 1687ms   (1125 × 1.5)
// 第5次重试:等待 2500ms   (1687 × 1.5,不超过 maxDelay)
// ...后续最大等待 5000ms

提示multiplier > 1 时为指数退避,multiplier = 1 时为固定间隔,不设置 multiplier 时默认不递增。

依赖引入

标准 Spring Boot 项目需显式引入 spring-retry 依赖:

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>

注意:Spring Retry 不在 Spring Boot 默认 starter 中,需显式引入。但以下情况可能无需手动引入:

  • 父 POM 已定义:项目继承的父 POM 已包含 spring-retry
  • 内部模块传递:公司内部公共模块已依赖 spring-retry

可通过 mvn dependency:tree 命令查看依赖树,确认是否已存在传递依赖。

版本兼容说明

@Retryable 注解属性在不同版本中存在差异:

版本 指定异常属性 说明
Spring Retry 1.x include 旧版本语法
Spring Retry 1.2+ value 推荐使用
Spring Retry 2.x retryFor 新版本别名

建议:统一使用 value 属性,兼容所有版本。

@Recover 方法签名规则

兜底方法必须遵循以下签名规范:

  1. 第一个参数必须是异常类型(与 @Retryable 指定的异常一致)
  2. 后续参数必须与原方法参数一致(顺序、类型、数量)
// 正确示例
@Recover
public void recoverUpdate(OptimisticLockingFailureException e, String orderId) { }

// 错误示例:参数顺序不一致
@Recover
public void recoverUpdate(String orderId, OptimisticLockingFailureException e) { }  // ❌

多 @Retryable 场景下的 @Recover 使用

当一个 Service 中存在多个 @Retryable 方法,且异常类型不同时,有两种处理方式:

方式一:每个异常类型单独定义兜底方法(推荐)

Spring Retry 会根据异常类型自动匹配对应的 @Recover 方法:

@Service
public class OrderService {
    
    // 方法1:乐观锁重试
    @Retryable(value = OptimisticLockingFailureException.class, maxAttempts = 3)
    public void updateOrder(String orderId) { }
    
    // 方法2:支付重试
    @Retryable(value = PaymentException.class, maxAttempts = 5)
    public void processPayment(String orderId) { }
    
    // 方法3:网络重试(多个异常)
    @Retryable(value = {ConnectException.class, SocketTimeoutException.class}, maxAttempts = 3)
    public void callExternalApi(String orderId) { }
    
    // 兜底方法1:处理 OptimisticLockingFailureException
    @Recover
    public void recoverUpdate(OptimisticLockingFailureException e, String orderId) {
        log.error("订单更新失败(乐观锁): {}", orderId);
        alertService.notify("乐观锁冲突", orderId);
    }
    
    // 兜底方法2:处理 PaymentException
    @Recover
    public void recoverPayment(PaymentException e, String orderId) {
        log.error("支付处理失败: {}", orderId);
        paymentFailureService.record(orderId, e.getMessage());
    }
    
    // 兜底方法3:处理网络异常(父类 Exception 可匹配多种子类)
    @Recover
    public void recoverApiCall(Exception e, String orderId) {
        log.error("外部API调用失败: {}, 异常类型: {}", orderId, e.getClass().getSimpleName());
        // 可根据异常类型进一步细分处理
        if (e instanceof ConnectException) {
            // 连接失败处理
        } else if (e instanceof SocketTimeoutException) {
            // 超时处理
        }
    }
}

方式二:统一兜底方法(按异常类型分发)

如果多个方法参数相同,可使用一个统一的方法处理:

@Service
public class OrderService {
    
    @Retryable(value = OptimisticLockingFailureException.class)
    public void updateOrder(String orderId) { }
    
    @Retryable(value = PaymentException.class)
    public void processPayment(String orderId) { }
    
    // 统一兜底方法(参数为 Exception 父类)
    @Recover
    public void recoverAll(Exception e, String orderId) {
        log.error("操作失败: {}, 异常: {}", orderId, e.getClass().getSimpleName());
        
        // 根据异常类型分发处理
        if (e instanceof OptimisticLockingFailureException) {
            // 乐观锁失败处理
            alertService.notify("乐观锁冲突", orderId);
        } else if (e instanceof PaymentException) {
            // 支付失败处理
            paymentFailureService.record(orderId, e.getMessage());
        } else {
            // 其他异常通用处理
            generalFailureService.record(orderId, e);
        }
    }
}

@Recover 匹配规则

规则 说明
异常类型匹配 Spring Retry 按异常类型选择最匹配的 @Recover 方法
精确匹配优先 如果存在精确异常类型的 @Recover,优先使用
父类匹配 无精确匹配时,使用能接收该异常的父类 @Recover
参数匹配 @Recover 方法的后续参数必须与原方法一致

提示:建议使用方式一(单独定义兜底方法),代码更清晰、更易维护。方式二适用于参数相同、处理逻辑相似的场景。


方案二:Guava Retryer

概述

Guava Retryer(实际来自 guava-retrying 库)提供灵活的程序化重试构建器,支持基于异常、基于返回结果等多种重试条件判断。

核心原理:通过 Builder 模式构建 Retryer,执行时按配置策略判断是否重试。

优缺点

优点 缺点
灵活的重试条件判断 需要手动编码,非注解驱动
支持基于返回值判断 需引入额外依赖
多种等待策略(固定、递增、随机) 与 Spring 集成需自行处理
支持重试监听器 代码侵入性较高

适用场景

  • 需要根据返回值判断是否重试(如返回 null、特定状态码)
  • 复杂重试逻辑组合
  • 非 Spring 项目
  • 需要精细控制重试过程

依赖引入

<dependency>
    <groupId>com.github.rholder</groupId>
    <artifactId>guava-retrying</artifactId>
    <version>2.0.0</version>
</dependency>

注意:此库虽以 "guava" 命名,但不是 Google Guava 官方组件,而是基于 Guava 的第三方重试库。

使用示例

import com.github.rholder.retry.*;

public class PaymentService {
    
    private final Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
        // 根据异常重试
        .retryIfExceptionOfType(PaymentException.class)
        // 根据返回值重试(返回 false 时重试)
        .retryIfResult(Predicates.equalTo(false))
        // 固定间隔等待
        .withWaitStrategy(WaitStrategies.fixedWait(500, TimeUnit.MILLISECONDS))
        // 最大重试5次
        .withStopStrategy(StopStrategies.stopAfterAttempt(5))
        // 重试监听器
        .withRetryListener(new RetryListener() {
            @Override
            public <V> void onRetry(Attempt<V> attempt) {
                log.info("第 {} 次重试", attempt.getAttemptNumber());
            }
        })
        .build();
    
    public boolean processPayment(PaymentRequest request) {
        try {
            return retryer.call(() -> paymentClient.pay(request));
        } catch (Exception e) {
            log.error("支付处理失败", e);
            return false;
        }
    }
}

方案三:Resilience4j 容错框架

概述

Resilience4j 是轻量级容错库,设计为 Netflix Hystrix 的替代方案。它提供模块化的容错组件,可灵活组合使用。

核心原理:通过装饰器模式包装函数,支持多层容错策略组合。

五大核心组件

组件 功能 核心作用
Retry 重试机制 处理短暂性故障
CircuitBreaker 熔断器 防止级联失败,快速失败
RateLimiter 限流器 控制请求速率,保护下游服务
Bulkhead 隔离舱 并发控制,资源隔离
TimeLimiter 超时控制 限制执行时间,防止长时间阻塞

优缺点

优点 缺点
模块化设计,按需引入 配置相对复杂
支持组件灵活组合 学习成本较高
轻量级,性能优于 Hystrix 文档相对较少
支持 Spring Boot Starter 监控需额外集成
函数式编程风格 注解方式灵活性有限

适用场景

  • 微服务架构容错保护
  • 外部服务调用保护
  • API 网关限流熔断
  • 高并发系统资源保护
  • 防止级联失败(雪崩效应)

核心组件详解

1. Retry(重试)

作用:处理短暂性故障,通过重试将暂时失败转化为成功。

核心参数

参数 含义 默认值 推荐值
maxAttempts 最大重试次数(含首次调用) 3 3-5
waitDuration 重试等待时间 500ms 根据场景调整
exponentialBackoffMultiplier 指数退避乘数 1(不递增) 2
retryExceptions 触发重试的异常类型 所有异常 指定具体异常
ignoreExceptions 不触发重试的异常类型 业务异常

使用场景

  • 网络抖动导致连接失败
  • 服务暂时不可达(503)
  • 数据库连接池瞬时耗尽
  • 乐观锁冲突
2. CircuitBreaker(熔断器)

作用:当失败率达到阈值时"熔断",快速失败,防止级联雪崩。

熔断器状态流转

CLOSED(关闭) ──失败率超阈值──→ OPEN(打开)
     ↑                              │
     │                              │ 等待时间后
     │                              ↓
     │                       HALF_OPEN(半开)
     │                              │
     │                              │
     └─────探测成功────←────探测失败────┘

核心参数

参数 含义 默认值 推荐值
failureRateThreshold 失败率阈值(百分比) 50% 50-80%
slidingWindowSize 滑动窗口大小(调用次数) 100 50-100
slidingWindowType 滑动窗口类型 COUNT_BASED COUNT_BASED
waitDurationInOpenState 熔断等待时间 60000ms 10-60s
permittedNumberOfCallsInHalfOpenState 半开状态允许调用次数 10 5-10
minimumNumberOfCalls 计算失败率的最小调用次数 100 10-50

使用场景

  • 外部服务故障保护(如支付网关)
  • 下游服务响应缓慢
  • 防止级联失败传播
  • API 网关熔断保护
3. RateLimiter(限流器)

作用:控制请求速率,防止下游服务过载。

限流原理:基于令牌桶/信号量,按时间周期分配调用许可。

核心参数

参数 含义 默认值 推荐值
limitForPeriod 一个周期内允许的调用次数 50 根据下游容量
limitRefreshPeriod 许可刷新周期 500ns 1s-1min
timeoutDuration 等待许可的超时时间 5s 0-5s

使用场景

  • API 接口限流(防刷)
  • 保护下游服务(如第三方 API)
  • 按用户/租户限流
  • 防止突发流量冲击
4. Bulkhead(隔离舱)

作用:限制并发调用数,隔离不同服务的资源,防止一个服务耗尽所有资源。

两种实现方式

方式 原理 适用场景
Semaphore(信号量) 限制并发线程数 无需线程池的场景
ThreadPool(线程池) 使用独立线程池 需要真正隔离的场景

核心参数

参数 含义 默认值 推荐值
maxConcurrentCalls 最大并发调用数 25 根据服务容量
maxWaitDuration 等待许可的最大时间 0(不等待) 0-5s
coreThreadPoolSize 核心线程数(线程池方式) - 根据场景
queueCapacity 队列容量(线程池方式) - 100

使用场景

  • 核心服务资源隔离(如支付服务)
  • 防止慢服务拖垮整个系统
  • 不同优先级服务隔离
  • 外部调用与内部调用隔离
5. TimeLimiter(超时控制)

作用:限制方法执行时间,防止长时间阻塞。

核心参数

参数 含义 默认值 推荐值
timeoutDuration 超时时间 1s 根据场景
cancelRunningFuture 超时后是否取消正在执行的任务 true true

使用场景

  • 外部 HTTP 调用超时保护
  • RPC 调用超时控制
  • 防止线程长时间阻塞

组合使用案例

案例1:Retry + CircuitBreaker(重试 + 熔断)

场景:调用外部支付网关,需要重试短暂故障,但连续失败时熔断保护。

效果

  • 短暂网络故障 → Retry 重试恢复
  • 服务持续故障 → CircuitBreaker 熔断,快速失败
  • 熔断后 → 等待时间后自动探测恢复
@Service
public class PaymentGatewayService {
    
    /**
     * 调用支付网关(重试 + 熔断组合)
     * 注解执行顺序:CircuitBreaker → Retry(从外到内,按照注解声明顺序装饰执行)
     */
    @CircuitBreaker(name = "paymentGateway", fallbackMethod = "paymentFallback")
    @Retry(name = "paymentGateway", fallbackMethod = "paymentFallback")
    public PaymentResult processPayment(PaymentRequest request) {
        log.info("调用支付网关: {}", request.getOrderId());
        return paymentClient.process(request);
    }
    
    /**
     * 兜底方法:熔断或重试耗尽时执行
     */
    private PaymentResult paymentFallback(PaymentRequest request, Exception e) {
        log.warn("支付网关调用失败,执行兜底: {}", e.getMessage());
        // 返回待处理状态,后续通过定时任务补偿
        return PaymentResult.pending(request.getOrderId(), "支付服务暂时不可用");
    }
}

// application.yml 配置
resilience4j:
  retry:
    instances:
      paymentGateway:
        maxAttempts: 3
        waitDuration: 500ms
        exponentialBackoffMultiplier: 2
        retryExceptions:
          - java.net.ConnectException
          - java.net.SocketTimeoutException
  circuitbreaker:
    instances:
      paymentGateway:
        failureRateThreshold: 50            # 失败率50%触发熔断
        slidingWindowSize: 10               # 最近10次调用计算失败率
        minimumNumberOfCalls: 5             # 至少5次调用才开始计算
        waitDurationInOpenState: 30s        # 熔断后等待30秒
        permittedNumberOfCallsInHalfOpenState: 3  # 半开状态允许3次探测
案例2:Retry + RateLimiter(重试 + 限流)

场景:调用第三方 API,需限流防止超配额,限流拒绝时重试等待。

效果

  • 正常请求 → RateLimiter 放行
  • 超出限流 → RateLimiter 拒绝,Retry 触发重试等待许可
  • 重试耗尽 → 返回兜底结果
@Service
public class ThirdPartyApiService {
    
    /**
     * 调用第三方API(限流 + 重试组合)
     * 注解执行顺序: RateLimiter → Retry(从外到内,按照注解声明顺序装饰执行)
     */
    @RateLimiter(name = "thirdPartyApi", fallbackMethod = "rateLimitFallback")
    @Retry(name = "thirdPartyApi", fallbackMethod = "retryFallback")
    public ApiResponse callThirdParty(String param) {
        log.info("调用第三方API: {}", param);
        return thirdPartyClient.request(param);
    }

    /**
     * 限流降级:直接返回,不触发重试
     */
    private ApiResponse rateLimitFallback(String param, RequestNotPermitted e) {
        log.warn("触发限流: {}", param);
        return ApiResponse.defaultResponse("系统繁忙,请稍后重试");
    }

    /**
     * 重试降级:业务调用失败后重试耗尽才执行
     */
    private ApiResponse retryFallback(String param, Exception e) {
        log.warn("重试耗尽: {}", e.getMessage());
        return ApiResponse.defaultResponse("服务暂时不可用");
    }
}

// application.yml 配置
resilience4j:
  retry:
    instances:
      thirdPartyApi:
        maxAttempts: 3
        waitDuration: 1s
        exponentialBackoffMultiplier: 2
  ratelimiter:
    instances:
      thirdPartyApi:
        limitForPeriod: 10              # 每周期允许10次调用
        limitRefreshPeriod: 1s          # 每秒刷新周期
        timeoutDuration: 0              # 不等待许可,直接拒绝
案例3:Retry + Bulkhead(重试 + 隔离)

场景:支付服务需要资源隔离,限制并发,失败时重试。

效果

  • 正常请求 → Bulkhead 放行
  • 并发超限 → Bulkhead 拒绝,Retry 触发重试等待
  • 重试耗尽 → 返回兜底结果
@Service
public class IsolatedPaymentService {
    
    /**
     * 支付处理(隔离 + 重试组合)
     * 限制支付服务并发,防止拖垮整个系统
     */
    @Bulkhead(name = "isolatedPayment", type = Bulkhead.Type.SEMAPHORE, fallbackMethod = "bulkheadFallback")
    @Retry(name = "isolatedPayment", fallbackMethod = "retryFallback")
    public PaymentResult processPayment(PaymentRequest request) {
        log.info("处理支付请求: {}", request.getOrderId());
        return paymentCoreService.process(request);
    }

    /**
     * 隔离舱降级:并发超限直接返回
     */
    private PaymentResult bulkheadFallback(PaymentRequest request, BulkheadFullException e) {
        log.warn("支付服务并发超限: {}", request.getOrderId());
        return PaymentResult.pending(request.getOrderId(), "支付服务繁忙,请稍后重试");
    }

    /**
     * 重试降级:业务异常重试耗尽后执行
     */
    private PaymentResult retryFallback(PaymentRequest request, Exception e) {
        log.warn("支付处理失败(重试耗尽): {}", request.getOrderId(), e);
        return PaymentResult.pending(request.getOrderId(), "支付服务暂时不可用");
    }
}

// application.yml 配置
resilience4j:
  retry:
    instances:
      isolatedPayment:
        maxAttempts: 3
        waitDuration: 200ms
  bulkhead:
    instances:
      isolatedPayment:
        maxConcurrentCalls: 5           # 最大5个并发调用
        maxWaitDuration: 0              # 不等待许可
案例4:全组件组合(完整容错保护)

场景:核心业务调用外部服务,需要完整容错保护。

效果

  • RateLimiter → 限制调用速率
  • Bulkhead → 限制并发数
  • TimeLimiter → 限制执行时间
  • CircuitBreaker → 失败率高时熔断
  • Retry → 短暂故障重试
@Service
public class CoreBusinessService {
    
    /**
     * 核心业务调用(完整容错组合)
     * 注解执行顺序(从外到内):Retry → CircuitBreaker → RateLimiter → Bulkhead → TimeLimiter
     */
    @Retry(name = "coreBusiness", fallbackMethod = "businessFallback")
    @CircuitBreaker(name = "coreBusiness")
    @RateLimiter(name = "coreBusiness")
    @Bulkhead(name = "coreBusiness", type = Bulkhead.Type.SEMAPHORE)
    @TimeLimiter(name = "coreBusiness")
    public CompletableFuture<BusinessResult> callExternalService(BusinessRequest request) {
        log.info("执行核心业务: {}", request.getId());
        return CompletableFuture.supplyAsync(() -> externalService.process(request));
    }
    
    private BusinessResult businessFallback(BusinessRequest request, Exception e) {
        log.error("核心业务调用失败: {}", e.getMessage());
        // 记录失败,后续补偿
        failureRecordService.record(request, e);
        return BusinessResult.failed("服务暂时不可用,已记录待补偿");
    }
}

// application.yml 完整配置
resilience4j:
  retry:
    instances:
      coreBusiness:
        maxAttempts: 3
        waitDuration: 500ms
        exponentialBackoffMultiplier: 2
  circuitbreaker:
    instances:
      coreBusiness:
        failureRateThreshold: 50
        slidingWindowSize: 20
        minimumNumberOfCalls: 10
        waitDurationInOpenState: 30s
        permittedNumberOfCallsInHalfOpenState: 5
  ratelimiter:
    instances:
      coreBusiness:
        limitForPeriod: 20
        limitRefreshPeriod: 1s
        timeoutDuration: 0
  bulkhead:
    instances:
      coreBusiness:
        maxConcurrentCalls: 10
        maxWaitDuration: 0
  timelimiter:
    instances:
      coreBusiness:
        timeoutDuration: 5s
        cancelRunningFuture: true

YAML 配置详解

推荐配置模板

resilience4j:
  # 重试配置
  retry:
    configs:
      default:                           # 默认配置
        maxAttempts: 3
        waitDuration: 500ms
        exponentialBackoffMultiplier: 2
        retryExceptions:
          - java.net.ConnectException
          - java.net.SocketTimeoutException
          - java.io.IOException
        ignoreExceptions:
          - com.yotexs.BusinessException  # 业务异常不重试
    instances:
      paymentService:
        baseConfig: default               # 继承默认配置
        maxAttempts: 5                    # 覆盖重试次数
      orderService:
        baseConfig: default

  # 熔断器配置
  circuitbreaker:
    configs:
      default:
        failureRateThreshold: 50
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 100
        minimumNumberOfCalls: 10
        waitDurationInOpenState: 30s
        permittedNumberOfCallsInHalfOpenState: 10
        recordExceptions:
          - java.net.ConnectException
          - java.net.SocketTimeoutException
        ignoreExceptions:
          - com.yotexs.BusinessException
    instances:
      criticalService:                    # 关键服务配置更严格
        failureRateThreshold: 30          # 失败率30%就熔断
        slidingWindowSize: 50
        waitDurationInOpenState: 60s      # 熔断等待更长

  # 限流器配置
  ratelimiter:
    configs:
      default:
        limitRefreshPeriod: 1s
        limitForPeriod: 50
        timeoutDuration: 0
    instances:
      apiGateway:
        limitForPeriod: 100               # API网关限流更宽松

  # 隔离舱配置
  bulkhead:
    configs:
      default:
        maxConcurrentCalls: 25
        maxWaitDuration: 0
    instances:
      paymentService:
        maxConcurrentCalls: 10            # 支付服务并发限制更严格

  # 超时配置
  timelimiter:
    configs:
      default:
        timeoutDuration: 5s
        cancelRunningFuture: true
    instances:
      slowService:
        timeoutDuration: 30s              # 慢服务超时更长

装饰器模式(函数式编程)

除注解方式外,Resilience4j 还支持函数式编程风格:

@Service
public class FunctionalStyleService {
    
    private final Retry retry;
    private final CircuitBreaker circuitBreaker;
    private final RateLimiter rateLimiter;
    private final Bulkhead bulkhead;
    
    public FunctionalStyleService() {
        // 从配置中获取实例
        this.retry = Retry.ofDefaults("functionalService");
        this.circuitBreaker = CircuitBreaker.ofDefaults("functionalService");
        this.rateLimiter = RateLimiter.ofDefaults("functionalService");
        this.bulkhead = Bulkhead.ofDefaults("functionalService");
    }
    
    /**
     * 装饰器模式组合(灵活控制)
     */
    public String executeWithDecorators(String param) {
        // 组合装饰器:从内到外包装
        Supplier<String> supplier = () -> externalService.call(param);
        
        // 依次装饰
        Supplier<String> decorated = 
            TimeLimiter.decorateFutureSupplier(timeout, () -> CompletableFuture.supplyAsync(supplier));
        decorated = Bulkhead.decorateSupplier(bulkhead, decorated);
        decorated = RateLimiter.decorateSupplier(rateLimiter, decorated);
        decorated = CircuitBreaker.decorateSupplier(circuitBreaker, decorated);
        decorated = Retry.decorateSupplier(retry, decorated);
        
        // 执行
        try {
            return decorated.get();
        } catch (Exception e) {
            return "fallback-result";
        }
    }
}

提示:函数式编程方式比注解方式更灵活,可动态调整配置,适合复杂场景。


三、异步重试方案

方案四:消息队列延迟重试

概述

消息队列重试利用 MQ 的延迟投递特性,将失败消息延迟后重新消费。常见实现方式:

  • RabbitMQ:死信队列(DLX)或延迟消息插件
  • RocketMQ:原生延迟消息级别

核心原理:消费失败 → 投递延迟队列 → 延迟到期 → 重新消费。

优缺点

优点 缺点
异步非阻塞,不占用业务线程 实时性较低
支持长时间间隔重试 需额外队列配置
消息持久化,不丢失 复杂度较高
可跨服务重试 消息顺序可能变化

适用场景

  • 外部 HTTP/RPC 服务调用失败
  • 跨系统消息处理失败
  • 需要长时间间隔重试(秒级到小时级)
  • 高并发场景避免线程阻塞

RabbitMQ 实现架构

┌─────────────┐     消费失败      ┌─────────────────┐
│  业务队列    │ ──────────────→ │  死信队列(DLX)   │
│  (topic)    │                  │  设置 TTL 延迟   │
└─────────────┘                  └─────────────────┘
       ↑                                   │
       │           延迟到期重新投递          │
       └───────────────────────────────────┘

关键代码示例

// 消费者配置
@Component
public class OrderConsumer {
    
    @RabbitListener(queues = "order.queue")
    public void processOrder(OrderMessage message, Channel channel, 
                             @Header(AmqpHeaders.DELIVERY_TAG) long tag) {
        try {
            orderService.process(message);
            channel.basicAck(tag, false);
        } catch (Exception e) {
            // 拒绝消息,路由到死信队列
            channel.basicReject(tag, false);
            log.warn("订单处理失败,进入重试队列: {}", message.getOrderId());
        }
    }
    
    @RabbitListener(queues = "order.retry.queue")
    public void retryProcess(OrderMessage message, Channel channel,
                             @Header(AmqpHeaders.DELIVERY_TAG) long tag,
                             @Header("x-death") List<Map<String, Object>> deathInfo) {
        int retryCount = extractRetryCount(deathInfo);
        if (retryCount > 5) {
            // 超过最大重试次数,记录失败
            failureLogService.record(message, "超过最大重试次数");
            channel.basicAck(tag, false);
            return;
        }
        
        try {
            orderService.process(message);
            channel.basicAck(tag, false);
        } catch (Exception e) {
            channel.basicReject(tag, false);
        }
    }
}

RabbitMQ 队列配置

@Configuration
public class RabbitConfig {
    
    @Bean
    public Queue orderQueue() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-dead-letter-exchange", "retry.exchange");
        args.put("x-dead-letter-routing-key", "order.retry");
        return new Queue("order.queue", true, false, false, args);
    }
    
    @Bean
    public Queue retryQueue() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-message-ttl", 30000);  // 30秒延迟
        args.put("x-dead-letter-exchange", "order.exchange");
        args.put("x-dead-letter-routing-key", "order.queue");
        return new Queue("order.retry.queue", true, false, false, args);
    }
}

RocketMQ 延迟消息示例

RocketMQ 提供原生延迟消息支持,无需额外配置死信队列:

@Component
public class RocketMQProducer {
    
    @Autowired
    private DefaultMQProducer producer;
    
    /**
     * 发送延迟消息
     * delayTimeLevel: 延迟级别
     * 1: 1s, 2: 5s, 3: 10s, 4: 30s, 5: 1min, 6: 2min, 7: 3min
     * 8: 4min, 9: 5min, 10: 6min, 11: 7min, 12: 8min, 13: 9min
     * 14: 10min, 15: 20min, 16: 30min, 17: 1h, 18: 2h
     */
    public void sendDelayMessage(String topic, Object message, int delayLevel) {
        Message msg = new Message(topic, JSON.toJSONString(message).getBytes());
        msg.setDelayTimeLevel(delayLevel);  // 设置延迟级别
        producer.send(msg);
    }
}

// 消费者重试示例
/**
 * 消息类需包含重试计数字段
 */
public class OrderMessage {
    private String orderId;
    private Integer retryCount;  // 重试次数存储在消息中(不能使用成员变量)
    // 其他业务字段...
}

@Component
@RocketMQMessageListener(topic = "order-topic", consumerGroup = "order-group")
public class RocketMQConsumer implements RocketMQListener<OrderMessage> {
    
    @Autowired
    private DefaultMQProducer producer;
    
    @Override
    public void onMessage(OrderMessage message) {
        try {
            orderService.process(message);
        } catch (Exception e) {
            // 从消息中获取重试次数
            int retryCount = message.getRetryCount() != null ? message.getRetryCount() : 0;
            
            if (retryCount < 5) {
                retryCount++;
                message.setRetryCount(retryCount);
                
                // 重新发送延迟消息(延迟级别递增)
                sendDelayMessage("order-topic", message, retryCount);
            } else {
                // 超过最大重试次数,记录失败
                failureLogService.record(message, "超过最大重试次数(5次)");
            }
        }
    }
    
    private void sendDelayMessage(String topic, OrderMessage message, int retryCount) {
        Message msg = new Message(topic, JSON.toJSONString(message).getBytes());
        // 延迟级别:第1次重试=级别4(30秒),第2次=级别5(1分钟),第3次=级别6(2分钟)...
        msg.setDelayTimeLevel(retryCount + 3);
        producer.send(msg);
    }
}

RabbitMQ vs RocketMQ 对比

特性 RabbitMQ RocketMQ
延迟实现 死信队列 + TTL 原生延迟级别
配置复杂度 较高(需配置 DLX) 低(直接设置级别)
延迟精度 可精确到毫秒 固定级别,精度较低
重试次数追踪 需通过 x-death 头获取 需自行维护

方案五:定时任务扫描重试

概述

将失败任务持久化到数据库,通过定时任务扫描并重试。适合重要业务场景,需要保证最终成功。

核心原理:失败任务入库 → 定时扫描 → 状态判断 → 重试执行 → 更新状态。

优缺点

优点 缺点
持久化存储,可追溯 实时性差
支持长时间间隔 需额外定时任务
支持手动干预 数据库压力
最终一致性保障 重试策略相对固定

适用场景

  • 重要业务操作(支付、结算)
  • 必须保证最终成功
  • 需要人工介入兜底
  • 跨天重试场景

实现思路

数据表设计

CREATE TABLE retry_task (
    id BIGINT PRIMARY KEY,
    task_type VARCHAR(50),      -- 任务类型
    task_data JSON,             -- 任务数据
    retry_count INT DEFAULT 0,  -- 重试次数
    max_retry INT DEFAULT 5,    -- 最大重试次数
    next_retry_time DATETIME,   -- 下次重试时间
    status VARCHAR(20),         -- PENDING/SUCCESS/FAILED
    error_message TEXT,         -- 错误信息
    created_time DATETIME,
    updated_time DATETIME
);

定时任务处理

@Component
public class RetryTaskScheduler {
    
    @Autowired
    private RedissonClient redissonClient;  // 分布式锁
    
    @Scheduled(cron = "0 */5 * * * ?")  // 每5分钟执行
    public void processRetryTasks() {
        // 分布式锁防止多节点重复执行
        RLock lock = redissonClient.getLock("retry:task:lock");
        try {
            // 尝试获取锁,最多等待5秒,锁持有时间10分钟
            if (!lock.tryLock(5, 600, TimeUnit.SECONDS)) {
                log.info("未获取到锁,跳过本次执行");
                return;
            }
            
            List<RetryTask> tasks = retryTaskRepository
                .findByStatusAndNextRetryTimeBefore("PENDING", LocalDateTime.now());
            
            for (RetryTask task : tasks) {
                processSingleTask(task);
            }
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    
    private void processSingleTask(RetryTask task) {
        try {
            executeTask(task);
            task.setStatus("SUCCESS");
            // 成功后无需增加重试计数
        } catch (Exception e) {
            task.setRetryCount(task.getRetryCount() + 1);
            task.setErrorMessage(e.getMessage());
            
            if (task.getRetryCount() >= task.getMaxRetry()) {
                task.setStatus("FAILED");
                alertService.notify(task);
            } else {
                // 设置下次重试时间(指数退避)
                int delayMinutes = (int) Math.pow(2, task.getRetryCount());
                task.setNextRetryTime(LocalDateTime.now().plusMinutes(delayMinutes));
            }
        }
        retryTaskRepository.save(task);
    }
    
    private void executeTask(RetryTask task) {
        switch (task.getTaskType()) {
            case "PAYMENT":
                paymentService.retry(task.getTaskData());
                break;
            case "NOTIFICATION":
                notificationService.retry(task.getTaskData());
                break;
            // 其他任务类型...
        }
    }
}

分布式部署注意事项

多节点部署时,定时任务扫描需考虑:

  • 分布式锁:使用 Redis/Redisson 防止重复执行
  • 乐观锁更新:使用版本号或 CAS 更新任务状态
  • 任务分片:按任务 ID 分片,不同节点处理不同分片

四、方案选型指南

4.1 方案对比表

方案 执行方式 实时性 资源占用 持久化 适用间隔 复杂度
Spring Retry 同步 毫秒级
Guava Retryer 同步 毫秒级
Resilience4j 同步 毫秒级
MQ 延迟重试 异步 秒级~小时
定时任务扫描 异步 分钟级~天

4.2 选型决策流程

                    是否需要持久化?
                          │
            ┌─────────────┴─────────────┐
            │                           │
           否                           是
            │                           │
     是否需要实时重试?              是否需要高实时性?
            │                           │
    ┌───────┴───────┐            ┌───────┴───────┐
    │               │            │               │
   是              否           否              是
    │               │            │               │
 Spring Retry    MQ延迟重试    定时任务扫描    MQ延迟重试
 Guava Retryer                  │               │
 Resilience4j               是否需要组合       配较短延迟
                            熔断/限流?
                                │
                        ┌───────┴───────┐
                        │               │
                       是              否
                        │               │
                  Resilience4j      Spring Retry

五、最佳实践建议

5.1 重试次数控制

  • 短暂性故障:3-5 次
  • 外部服务调用:3-5 次,配合熔断
  • 重要业务:可设置更多次数,但需设置告警

5.2 退避策略选择

策略 适用场景
固定间隔 快速恢复场景(乐观锁)
线性递增 网络恢复场景
指数递增 外部服务恢复、避免雪崩
随机抖动 高并发场景分散重试时间

5.3 重试边界界定

必须重试

  • 网络超时、连接异常
  • 服务暂时不可用(503)
  • 乐观锁冲突、死锁
  • 限流触发(429)

禁止重试

  • 业务校验失败(参数错误)
  • 权限不足(401/403)
  • 业务规则冲突
  • 明确的永久性错误

5.4 兜底处理

任何重试机制都应有兜底方案:

  • 记录失败日志:便于问题排查
  • 发送告警通知:运维介入处理
  • 降级处理:返回默认值或缓存的旧数据
  • 人工介入:重要业务支持手动重试

5.5 幂等性设计(重试必备)

⚠️ 重要:重试机制必须配合幂等性设计,否则可能导致数据重复处理。

幂等性定义:同一操作执行多次与执行一次的效果相同。

常见幂等性实现方式

方式 适用场景 实现复杂度
唯一业务标识 支付、订单创建
状态机控制 状态流转(待支付→已支付)
数据库唯一约束 防止重复插入
乐观锁 更新操作
Token 机制 API 接口调用
分布式锁 高并发场景

幂等性代码示例

// 1. 唯一业务标识实现幂等
@Service
public class PaymentService {
    
    @Transactional
    public void pay(PaymentRequest request) {
        // 检查是否已处理(幂等判断)
        Optional<Payment> existing = paymentRepository.findByRequestId(request.getRequestId());
        if (existing.isPresent()) {
            log.info("支付请求已处理,跳过: {}", request.getRequestId());
            return;  // 直接返回,不重复处理
        }
        
        // 创建支付记录
        Payment payment = new Payment();
        payment.setRequestId(request.getRequestId());
        payment.setAmount(request.getAmount());
        payment.setStatus("PAID");
        paymentRepository.save(payment);
    }
}

// 2. Token 机制实现 API 幂等
@RestController
public class ApiController {
    
    @PostMapping("/api/order")
    public Result createOrder(@RequestBody OrderRequest request, 
                              @RequestHeader("Idempotent-Token") String token) {
        // 验证并消费 Token
        if (!redisTemplate.delete("idempotent:" + token)) {
            return Result.error("重复请求或 Token 无效");
        }
        
        // 处理业务逻辑
        return orderService.createOrder(request);
    }
}

// Token 生成接口(客户端先获取 Token)
@GetMapping("/api/token")
public String generateToken() {
    String token = UUID.randomUUID().toString();
    redisTemplate.opsForValue().set("idempotent:" + token, "1", 10, TimeUnit.MINUTES);
    return token;
}

// 3. 状态机实现幂等
@Service
public class OrderService {
    
    @Transactional
    public void payOrder(String orderId) {
        Order order = orderRepository.findById(orderId);
        
        // 状态判断(幂等判断)
        if (!"WAIT_PAY".equals(order.getStatus())) {
            log.info("订单状态非待支付,跳过: {}, 当前状态: {}", orderId, order.getStatus());
            return;
        }
        
        // 状态流转:待支付 → 已支付
        order.setStatus("PAID");
        order.setPayTime(LocalDateTime.now());
        orderRepository.save(order);
    }
}

幂等性与重试机制配合要点

  1. 支付类操作:使用唯一请求 ID,重试时携带相同 ID
  2. 订单创建:客户端生成唯一订单号,服务端校验唯一性
  3. 状态更新:前置状态判断,确保状态流转方向正确
  4. 消息消费:消息携带唯一 ID,消费前检查是否已处理
  5. API 接口:Token 机制,先获取 Token 后携带 Token 调用

结语

重试机制是分布式系统容错设计的基础能力。选择合适的重试方案,需要在实时性、资源占用、持久化需求、复杂度之间权衡。对于大多数业务场景,Spring Retry 提供的同步重试已足够;对于高并发或跨系统场景,消息队列延迟重试是更优选择;对于核心业务,定时任务扫描重试提供最终一致性保障。

合理使用重试机制,可以显著提升系统稳定性,但也要避免滥用——重试不是解决所有问题的万能钥匙,错误的边界界定可能导致问题掩盖或雪崩效应。

posted @ 2026-05-10 17:57  flycloudy  阅读(2)  评论(0)    收藏  举报