一次 Redisson 升级排查
Redisson版本:4.3.0
起因是在配置 Redisson 的 Config 的设置重试延迟时间方法 setRetryDelay 时,
发现原本的 setRetryInterval(int retryInterval) 变成了 ,setRetryInterval(int retryInterval)
点进源码一看:
/*** Use {@link #setRetryDelay(DelayStrategy)} instead.** @param retryInterval - time in milliseconds* @return config*/@Deprecatedpublic T setRetryInterval(int retryInterval)原来是官方废弃了这个方法,让我用一个叫做 setRetryDelay(DelayStrategy) 的新方法,
那么我们直接跳转到这个新方法的定义,看看它的注释:
/** * Defines the delay strategy for a new attempt to send a command. * <p> * Default is <code>EqualJitterDelay(Duration.ofSeconds(1), Duration.ofSeconds(2))</code> * * @see DecorrelatedJitterDelay * @see EqualJitterDelay * @see FullJitterDelay * @see ConstantDelay * * @param retryDelay delay strategy implementation * @return options instance */public T setRetryDelay(DelayStrategy retryDelay)看来是 setRetryDelay 不让我直接传 int 参数了,而是要我传 DelayStrategy 参数,而且这个参数还是个接口,
说明我得传一个实现类进去,注释里还说目前这个接口有以下四种实现类
ConstantDelay:固定延迟,每次重试都等同样长时间EqualJitterDelay:等分抖动延迟,Redisson 当前默认策略。保留一半的指数退避值,再给另一半加随机抖动值FullJitterDelay:完全随机抖动延迟,对指数退避结果做完全随机抖动化DecorrelatedJitterDelay:相关抖动延迟,延迟会指数式增长,但抖动值会受前一次退避时长影响
上面的描述来自官方文档,除了 ConstantDelay 非常的简单易懂,其他三个都有点抽象,
不过我注意到,这三个重试策略中的前两个都提到了一个词:指数退避,那我高低得从这里切入。
指数退避
指数退避,是一种失败后逐步拉长重试等待时间的机制。可以把它理解成:
第一次失败,等 1 秒
第二次失败,等 2 秒
第三次失败,等 4 秒
第四次失败,等 8 秒
…
也就是等待时间不是线性增加,而是按 2 的幂 这样越来越大,所以叫“指数”退避。
为什么要这样做呢?
因为如果一个服务已经出问题了,客户端还用很高频率不断重试,就容易出现两个问题:
- 把服务压得更厉害:服务本来就在抖动、切换、恢复,客户端越频繁重试,它越难缓过来。
- 大量客户端同时重试:会形成“重试风暴”,一下子又把 Redis、数据库、接口打满。
指数退避的核心目的,就是失败越多,说明问题越可能不是服务器瞬时抖动,而是真故障,那客户端就应该等得更久一点。
一个直观的例子
假设程序访问 Redis 失败了。而你设置重试策略为固定每隔 1 秒重试一次,
如果有 100 台机器都这样干,就可能每秒一起冲 Redis 一次。
但如果用指数退避,这样重试频率会越来越低,系统就有恢复空间。
在代码中,这个指数退避值的计算是如何实现的?
首先从 DelayStrategy 接口看起
public interface DelayStrategy { /** * Calculates the delay duration to wait before the next retry attempt. * * @param attempt the zero-based retry attempt number (0 = first retry) * @return the duration to wait before the next retry attempt */ Duration calcDelay(int attempt);}可以看到,这个接口里只有一个 calcDelay 方法,接收一个 attempt 参数,表示重试的次数。
返回一个 Duration 类型,表示等待时间,另外三种策略都是通过实现这个方法来计算等待时间的。
在 calcDelay 方法中,首先需要显式指定延迟时间的最小值(baseMs/minDelay)和最大值(maxMs/maxDelay),
然后,定义一个 exponentialDelay 变量,用来存放计算好的指数退避值,计算方法如下:
随机抖动
真实系统里,往往不是只做指数退避,还会再加一个 jitter(随机抖动值)。
原因在于,如果所有客户端都严格按 1、2、4、8 秒去重试,它们还是可能在同一时刻一起打过去。
所以更常见的是:
- 大方向按指数增长
- 具体每次等待再加一点随机性
当按照上面计算出指数退避值后,接下来就是对指数退避值进行抖动处理,这里 Redisson 提出了两种策略:
EqualJitterDelay:等分抖动延迟,Redisson 当前默认策略。保留一半的指数退避值,再给另一半加随机抖动值FullJitterDelay:完全随机抖动延迟,对指数退避结果做完全随机抖动化
首先来看下 EqualJitterDelay,它的核心逻辑如下:
// 保留一半的指数退避值long halfDelay = exponentialDelayMs / 2;// 在 [0, halfDelay] 之间生成一个随机抖动值long randomComponent = random(0, halfDelay + 1);// 将保留的一半指数退避值和随机抖动值相加,得到最终的等待时间return halfDelay + randomComponent;另一种策略 FullJitterDelay 的实现如下:
// 在 [0, exponentialDelayMs] 之间生成一个随机抖动值long jitteredDelayMs = random(0, exponentialDelayMs + 1);// 直接返回这个随机抖动值return jitteredDelayMs;最终,EqualJitterDelay 的等待时间范围为,
而 FullJitterDelay 的等待时间范围为。
这两种策略分别适用于什么场景呢?
- 如果你想追求稳定性,可以选择
EqualJitterDelay策略,因为它会保留一半的指数退避值,再给另一半加随机抖动值,所以等待时间会比较稳定。 - 如果你想追求随机性,可以选择
FullJitterDelay策略,因为它会对指数退避结果做完全随机抖动化,所以等待时间会比较随机。
基于前一次结果的随机退避
现在来看最后一个策略:DecorrelatedJitterDelay。
这个策略和前两个策略最大的不同在于,它的退避时间不跟重试次数 attempt 挂钩了,而是只跟上一次重试时间 previousDelay 挂钩,
前两个策略计算退避时间时,使用的是 ,而该策略计算退避时间,使用的是如下核心逻辑:
// 1. 第一次调用时,因为 previousDelay 还是 0,系统会先把 prev 取成 minDelay;// 之后调用时,将prev设置为上一次重试时间long prev = (previousDelay == 0 ? minDelay : previousDelay)// 2. 在 [0, 3倍上次重试时间] 之间生成一个随机值long randomComponent = random(0, prev * 3)// 3. 在随机值上加一个最小重试时间,然后和最大重试时间取最小值long newDelay = min(minDelay + randomComponent, maxDelay)// 4. 更新上次重试时间previousDelay = newDelayreturn newDelay按照如此逻辑执行,第一次重试时间会落在 这个区间内; 后面每次都会在 0和上一次结果的 3 倍 随机滚动。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!