【Spring】退避抖动算法

本文深入探讨了指数退避算法的原理与应用,通过实例展示了算法如何在重试失败操作时增加延迟,以减轻网络拥堵并提高成功率。文章还提供了Java和Spring中实现该算法的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

指数退避的原理是对于连续错误响应,重试等待间隔越来越长。
您应该实施最长延迟间隔最大重试次数。最长延迟间隔和最大重试次数不一定是固定值,并且应当根据正在执行的操作和其他本地因素(例如网络延迟)进行设置。

大多数指数退避算法会利用抖动(随机延迟)来防止连续的冲突。
由于在这些情况下您并未尝试避免此类冲突,因此无需使用此随机数字。但是,如果使用并发客户端,抖动可帮助您更快地成功执行请求。

至于指数避退算法的场景有哪些呢?指数退避算法在计算机网络中应用很广泛,这里简单说两个场景,第一个场景,接入三方支付服务,在三方支付提供的接入接口规范中,服务方交易结束结果通知和商户主动查询交易结果都用到重发机制,这就是所谓的退避算法,这地方其实也引出了另一个知识点——接口的幂等性( 使用相同参数对同一资源重复调用某个接口的结果与调用一次的结果相同),这里不再过多赘述。第二个场景,在app应用中,很多场景会遇到轮询一类的问题,一般的轮询对于app性能和电量的消耗都是个巨大的灾难。那如何解决这种问题呢?app在上一次更新操作之后还未被使用的情况下,使用指数退避算法来减少更新频率,从而节省资源和减少电的消耗。

以下代码演示如何在 Java 中实施此增量延迟。

public class RetryDemo {

    // 最长延迟间隔,单位是毫秒
    private static int MAX_WAIT_INTERVAL = 100000;
    // 最大重试次数
    private static int MAX_RETRIES = 5;

    public enum Results {
        SUCCESS,
        NOT_READY,
        THROTTLED,
        SERVER_ERROR
    }

    public static void main(String[] args) {
        doOperationAndWaitForResult();
    }

    // 指数退避 算法
    public static void doOperationAndWaitForResult() {

        try {
            int retries = 0;
            boolean retry = false;

            do {
                long waitTime = Math.min(getWaitTimeExp(retries), MAX_WAIT_INTERVAL);

                System.out.print("等待时间:" + waitTime + " ms \n");

                // Wait for the result.
                Thread.sleep(waitTime);

                // Get the result of the asynchronous operation.
                Results result = getAsyncOperationResult();

                if (Results.SUCCESS == result) {
                    retry = false;
                } else if (Results.NOT_READY == result) {
                    retry = true;
                } else if (Results.THROTTLED == result) {
                    retry = true;
                } else if (Results.SERVER_ERROR == result) {
                    retry = true;
                }
                else {
                    retry = false;
                }

            } while (retry && (retries++ < MAX_RETRIES));
        }
        catch (Exception ex) {
        }
    }

    // 假设每次都返回 SERVER_ERROR
    public static Results getAsyncOperationResult() {
        return Results.SERVER_ERROR;
    }

    // 根据重试的次数,返回 2 的指数的等待时间,单位是毫秒
    public static long getWaitTimeExp(int retryCount) {

        long waitTime = ((long) Math.pow(2, retryCount) * 100L);

        return waitTime;
    }

}

输出如下:

等待时间:100 ms 
等待时间:200 ms 
等待时间:400 ms 
等待时间:800 ms 
等待时间:1600 ms 
等待时间:3200 ms 

以下代码演示如何在Spring中实现退避(指数时间间隔)。

    public static void main(String[] args) {

        long initialInterval = 100;//初始间隔

        long maxInterval = 8 * 1000L;//最大间隔

        long maxElapsedTime = 50 * 1000L;//最大时间间隔

        double multiplier = 1.5;//递增倍数(即下次间隔是上次的多少倍)

        ExponentialBackOff backOff = new ExponentialBackOff(initialInterval, multiplier);
        backOff.setMaxInterval(maxInterval);
        //currentElapsedTime = interval1 + interval2 + ... + intervalN;
        backOff.setMaxElapsedTime(maxElapsedTime);
        BackOffExecution execution = backOff.start();

        for(int i = 1; i <= 18; i++) {
            if(execution.nextBackOff() == BackOffExecution.STOP){
                break;
            }
            System.out.println("i: " + i + ", wait time: " + execution.nextBackOff());
        }
    }

输入如下:

i: 1, wait time: 150
i: 2, wait time: 337
i: 3, wait time: 757
i: 4, wait time: 1702
i: 5, wait time: 3829
i: 6, wait time: 8000
i: 7, wait time: 8000
i: 8, wait time: 8000

以下代码演示如何在Spring中实现退避(固定时间间隔)。

    public static void main(String[] args) {

        long initialInterval = 100;//初始间隔

        long maxAttempts = 10;

        BackOff backOff = new FixedBackOff(initialInterval, maxAttempts);

        BackOffExecution execution = backOff.start();

        for(int i = 1; i <= 10; i++) {
            if(execution.nextBackOff() == BackOffExecution.STOP){
                break;
            }
            System.out.println("i: " + i + ", wait time: " + execution.nextBackOff());
        }
    }

输入如下:

i: 1, wait time: 100
i: 2, wait time: 100
i: 3, wait time: 100
i: 4, wait time: 100
i: 5, wait time: 100

 

<think>好的,我需要分析用户的问题,看看用户想知道是否可以直接使用ExponentialRandomBackOffPolicy来达到与之前配置的RetrySpec相同的效果。首先,用户可能是在比较两种不同的重试策略实现,可能是在Spring Retry项目中遇到的选择问题。 用户之前使用的RetrySpec配置了退避策略,包含最大尝试次数、最小和最大退避时间、抖动因子以及异常过滤。现在他提到ExponentialRandomBackOffPolicy,这个属于Spring Retry中的退避策略类。我需要确认这两个组件是否功能对等,或者是否有差异需要注意。 首先,我需要回忆Spring Retry中的RetryTemplate和BackOffPolicy。ExponentialRandomBackOffPolicy是Spring Retry中的一个退避策略实现,确实提供了指数退避和随机抖动的功能。而用户之前使用的RetrySpec可能属于另一个库,比如Reactor的Retry工具,或者是其他响应式库中的重试配置。这两个库可能在实现细节上有所不同,比如参数名称、默认值或计算方法。 接下来,我需要比较两者的配置参数和功能。用户提到的RetrySpec配置中的参数包括maxAttempts(最大尝试次数)、minBackoff(最小退避时间)、maxBackoff(最大退避时间)、jitter(抖动因子)。而ExponentialRandomBackOffPolicy在Spring Retry中有类似的参数,比如initialInterval(初始间隔,对应minBackoff)、maxInterval(最大间隔,对应maxBackoff),multiplier(乘数,通常是2,用于指数递增),以及random(用于抖动,可能是一个布尔值或随机因子)。 但要注意的是,ExponentialRandomBackOffPolicy的抖动实现可能不同。例如,在Spring Retry中,当启用随机抖动时,退避时间的计算可能是在当前间隔的基础上乘以一个随机因子(比如1±jitter),而用户之前的配置中的jitter可能是一个范围值,比如0到1,用来控制抖动的幅度。需要确认两者的抖动计算方式是否一致,否则可能导致重试时间不同。 另外,RetrySpec中的maxAttempts是直接配置在RetrySpec上的,而ExponentialRandomBackOffPolicy可能不直接处理最大尝试次数,这部分可能由RetryTemplate的RetryPolicy(如SimpleRetryPolicy)来控制。因此,用户需要同时配置RetryPolicy和BackOffPolicy才能达到相同的效果,而不仅仅是更换退避策略。 还需要注意异常过滤的部分。在之前的RetrySpec中,使用filter(this::isRetryException)来决定哪些异常需要重试。而在Spring Retry中,这通常是通过RetryPolicy的retryFor方法或通过指定可重试的异常类型来实现的。因此,用户需要确保在切换使用ExponentialRandomBackOffPolicy的同时,正确配置RetryPolicy中的异常判断逻辑,以保持一致性。 此外,时间单位也是一个需要注意的点。需要确认两者的配置参数是否使用相同的时间单位(如毫秒或秒),否则可能导致退避时间不符合预期。 总结来说,虽然ExponentialRandomBackOffPolicy和RetrySpec都提供了指数退避抖动功能,但由于它们属于不同的库或框架,具体的配置参数、默认值、抖动算法以及与其他组件的整合方式可能存在差异。用户需要详细对比两者的参数和实现细节,并进行适当的配置调整,才能确保两者的效果完全一致。建议用户查阅相关文档,编写单元测试来验证重试行为是否符合预期。</think>以下是两种实现方式的对比分析及迁移建议: $\boxed{\text{不完全等同,需注意5个关键差异点}}$ ```java // 使用Spring Retry的等效配置示例 RetryTemplate template = new RetryTemplate(); // 1. 退避策略配置(核心差异点) ExponentialRandomBackOffPolicy backOffPolicy = new ExponentialRandomBackOffPolicy(); backOffPolicy.setInitialInterval(backoffProperties.getMinBackoff().toMillis()); // 注意单位转换 backOffPolicy.setMaxInterval(backoffProperties.getMaxBackoff().toMillis()); backOffPolicy.setMultiplier(2); // 硬编码的指数基数 backOffPolicy.setRandom(new JitterRandom(backoffProperties.getJitter())); // 需自定义抖动实现 // 2. 重试次数控制(分离式配置) SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(); retryPolicy.setMaxAttempts(maxAttempts); template.setRetryPolicy(retryPolicy); template.setBackOffPolicy(backOffPolicy); // 3. 异常过滤配置(需通过RetryPolicy实现) retryPolicy.setRetryableExceptions(RetryableException.class, TimeoutException.class); ``` ### 关键差异对照表 | 特性 | 原RetrySpec实现 | Spring Retry的ExponentialRandomBackOffPolicy | |--------------------|----------------------------------------|--------------------------------------------------| | **重试次数控制** | 通过`.backoff(max,...)`直接设置 | 需单独配置RetryPolicy(如SimpleRetryPolicy) | | **指数基数** | 固定为2(隐式) | 通过setMultiplier()显式设置(默认2) | | **抖动算法** | 抖动范围:$[-jitter, +jitter]$ | 默认实现为$\pm$当前间隔的50%(需自定义实现才能完全等效) | | **时间单位** | 依赖Duration类型(自动处理单位) | 需手动转换单位为毫秒(如setInitialInterval(1000)表示1秒) | | **异常过滤机制** | 通过.filter()方法链式配置 | 需通过RetryPolicy的retryFor/notRetryOn方法配置 | ### 等效实现必要条件 1. **自定义抖动适配器**(核心差异点) 需实现如下抖动算法适配: ```java public class JitterRandom extends Random { private final double jitterFactor; public JitterRandom(double jitter) { this.jitterFactor = jitter; } // 生成[-jitter, +jitter]区间的随机因子 public double nextJitter() { return (1.0 - jitterFactor) + nextDouble() * 2 * jitterFactor; } } ``` 2. **时间单位转换验证** 假设原配置中`backoffProperties.getMinBackoff()`返回的是Duration类型: ```java // 示例:将Duration转换为毫秒 backOffPolicy.setInitialInterval(backoffProperties.getMinBackoff().toMillis()); ``` 3. **异常类型精确匹配** 如果原`isRetryException`包含嵌套异常判断(如`throwable.getCause() instanceof TimeoutException`),需通过`RetryPolicyBuilder`配置: ```java RetryPolicyBuilder.custom() .retryOn(throwable -> { Throwable root = ExceptionUtils.getRootCause(throwable); return root instanceof RetryableException || root instanceof TimeoutException; }) .build(); ``` ### 验证建议 建议通过以下测试用例验证等效性: ```java // 给定参数:maxAttempts=3, min=100ms, max=1000ms, jitter=0.5 @Test void testBackoffBehavior() { BackOffContext context = backOffPolicy.start(null); // 第一次重试 long delay1 = backOffPolicy.nextBackOff(context); assertThat(delay1).isBetween(50L, 150L); // 100 * (1 ± 0.5) // 第二次重试(指数增长) long delay2 = backOffPolicy.nextBackOff(context); assertThat(delay2).isBetween(100L, 300L); // 200 * (1 ± 0.5) // 达到max后不再增长 for (int i = 0; i < 10; i++) { backOffPolicy.nextBackOff(context); } long finalDelay = backOffPolicy.nextBackOff(context); assertThat(finalDelay).isLessThanOrEqualTo(1500L); // 1000 * 1.5 } ``` 最终结论:虽然可以通过精细配置达到相似效果,但需要额外处理抖动算法、异常过滤等细节,直接替换可能引入细微行为差异。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值