guava-retrying 初体验

一、前言

由于最近项目中用到了guava-retrying,并且以前没有遇到过这个工具包,所以准备通过本篇文章来系统的梳理下该工具包的使用。

二、背景

  一般在各种业务场景中,为了保持系统稳定,我们都会有相应的重试机制,因为比如说,某个接口某个数据库链接由于网络抖动或者其他因素导致响应失败,这时候直接判定失败或者Mock数据未必是一种优雅的方式,因为这种情况下未必是接口挂掉了或者数据库连不上了,有可能是网络一时的抖动导致的,所以这时候一个优雅的重试机制或许能帮上我们。

三、实现

guava-retrying是Google Guava库的一个扩展包,可以为任意函数调用创建可配置的重试机制。该扩展包比较简单,大约包含了10个方法和类:

github地址:https://github.com/rholder/guava-retrying

不过可以看到github上该项目已经好多年没有维护了,但这并不影响它的使用,因为它已经足够稳定了。接下来,我们直接来根据个例子来学习。

首先,看下Maven配置:

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

因为guava-retrying是基于Google的核心类库guava的重试机制实现,所以需要依赖guava的包,这里记得引入下。例子如下:

Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
        .retryIfException()
        .retryIfResult(aBoolean -> Objects.equals(aBoolean, false))
        .withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(10, TimeUnit.SECONDS, Executors.newCachedThreadPool()))
        .withWaitStrategy(WaitStrategies.fixedWait(5, TimeUnit.SECONDS))
        .withStopStrategy(StopStrategies.stopAfterAttempt(5))
        .withRetryListener(new RetryListener() {
            @Override
            public <V> void onRetry(Attempt<V> attempt) {
                System.out.print("retry time=" + attempt.getAttemptNumber());
            }
        }).build();
try {
    retryer.call(() -> {
        // 逻辑处理
        return null;
    });
} catch (Exception e) {
    System.out.println("exception:" + e);
}

这就是一个重试机制的实现了,比较简单,我们来看下具体接口及相应的策咯。

  • newBuilder:创建RetryerBuilder对象,通过该类进行构建各种重试策咯;
  • retryIfException:抛出异常时重试,但抛出error不会重试;另外该方法还包含一个重载的方法,可以自定义针对异常的实现;
  • retryIfRuntimeException:见名知义,抛出RuntimeException时重试;
  • retryIfExceptionOfType:抛出指定异常类型时重试;
  • retryIfResult:根据具体的返回值选择重试;
  • withRetryListener:在重试的时候进行事件监听,这中间我们可以记录下错误日志什么的;可以注册多个事件监听器,会按照注册顺序依次调用;
  • withWaitStrategy:重试等待策略,核心策咯之一;
  • withStopStrategy:重试停止策略,核心策咯之一;
  • withBlockStrategy:重试阻塞策略,也就是两次重试的时间间隔的实现方式;
  • withAttemptTimeLimiter:单次任务执行时长限制(如果单次任务执行超时,则终止执行当前任务)(该方法因SimpleTimeLimiter构造函数变更已失效无法使用);
  • build:通过newBuilder构建了各种重试策咯,构建完成,还需要通过build方法借助Retryer来执行;

接下来,我们来看一下主要的几个策咯及核心类。

1. Attemp

Attemp既是一次任务重试(call),也是一次请求的结果,记录了当前请求的重试次数,是否包含异常和请求的返回值。我们可以配合监听器使用,用于记录重试过程的细节,常用的方法有如下几个:

  • getAttemptNumber(),表示准备开始第几次重试;
  • getDelaySinceFirstAttempt(),表示距离第一次重试的延迟,也就是与第一次重试的时间差,单位毫秒;
  • hasException(),表示是异常导致的重试还是正常返回;
  • hasResult(),表示是否返回了结果;因为有时候是因为返回了特定结果才进行重试;
  • getExceptionCause(),如果是异常导致的重试,那么获取具体具体的异常类型;
  • getResult(),返回重试的结果;
  • get(),如果有的话,返回重试的结果;和getResult不同的在于对异常的处理;
2. Retryer

Retryer是最核心的类,是用于执行重试策咯的类,通过RetryerBuilder类进行构造,并且RetryerBuilder负责将设置好的重试策咯添加到Retryer中,最终通过执行Retryer的核心方法call来执行重试策咯:

public V call(Callable<V> callable) throws ExecutionException, RetryException {
    long startTime = System.nanoTime();
    // 重试次数
    for (int attemptNumber = 1; ; attemptNumber++) {
        Attempt<V> attempt;
        try {
            // attemptTimeLimiter会设置业务执行的时长限制
            V result = attemptTimeLimiter.call(callable);
            // 根据上次执行结果构建新的重试对象
            attempt = new ResultAttempt<V>(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
        } catch (Throwable t) {
            // 如果有异常,构建新的异常重试对象
            attempt = new ExceptionAttempt<V>(t, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
        }

        // 循环遍历监听器
        for (RetryListener listener : listeners) {
            listener.onRetry(attempt);
        }

        // 判断是否满足重试条件,来决定是否继续等待并进行重试
        if (!rejectionPredicate.apply(attempt)) {
            return attempt.get();
        }
        // 达到停止重试策略,但还没有结果,抛出异常
        if (stopStrategy.shouldStop(attempt)) {
            throw new RetryException(attemptNumber, attempt);
        } else {
            // 获取等待策略中设置的重试的时长
            long sleepTime = waitStrategy.computeSleepTime(attempt);
            try {
                // 阻塞策略进行阻塞
                blockStrategy.block(sleepTime);
            } catch (InterruptedException e) {
                // 线程中断,抛出异常
                Thread.currentThread().interrupt();
                throw new RetryException(attemptNumber, attempt);
            }
        }
    }
}
3. WaitStrategies 重试等待策略
3.1 ExponentialWaitStrategy 指数等待策略

指数补偿 算法 Exponential Backoff

.withWaitStrategy(WaitStrategies.exponentialWait(100, 5, TimeUnit.MINUTES))

创建一个永久重试的重试器,每次重试失败时以递增的指数时间等待,直到最多5分钟。 5分钟后,每隔5分钟重试一次。对该例而言:

第一次失败后,依次等待时长:2^1 * 100;2^2 * 100;2^3 * 100;...

在ExponentialWaitStrategy中,根据重试次数计算等待时长的源码我们可以关注下:

@Override
public long computeSleepTime(Attempt failedAttempt) {
    double exp = Math.pow(2, failedAttempt.getAttemptNumber());
    long result = Math.round(multiplier * exp);
    if (result > maximumWait) {
        result = maximumWait;
    }
    return result >= 0L ? result : 0L;
}

如果以后有类似的需求,我们可以自己写下这些算法,而有关更多指数补偿 算法 Exponential Backoff,可以参考:http://en.wikipedia.org/wiki/Exponential_backoff


3.2 FibonacciWaitStrategy 斐波那契等待策略

Fibonacci Backoff 斐波那契补偿算法

.withWaitStrategy(WaitStrategies.fibonacciWait(100, 2, TimeUnit.MINUTES))

创建一个永久重试的重试器,每次重试失败时以斐波那契数列来计算等待时间,直到最多2分钟;2分钟后,每隔2分钟重试一次;对该例而言:

第一次失败后,依次等待时长:1*100;1*100;2*100;3*100;5*100;...

3.3 FixedWaitStrategy 固定时长等待策略
withWaitStrategy(WaitStrategies.fixedWait(10,  TimeUnit.SECONDS))

固定时长等待策略,失败后,将等待固定的时长进行重试;


3.4 RandomWaitStrategy 随机时长等待策略
withWaitStrategy(WaitStrategies.randomWait(10,  TimeUnit.SECONDS));
withWaitStrategy(WaitStrategies.randomWait(1,  TimeUnit.SECONDS, 10, TimeUnit.SECONDS));

随机时长等待策略,可以设置一个随机等待的最大时长,也可以设置一个随机等待的时长区间。


3.5 IncrementingWaitStrategy 递增等待策略
withWaitStrategy(WaitStrategies.incrementingWait(1,  TimeUnit.SECONDS, 5, TimeUnit.SECONDS))

递增等待策略,根据初始值和递增值,等待时长依次递增。就本例而言:

第一次失败后,将依次等待1s;6s(1+5);11(1+5+5)s;16(1+5+5+5)s;...


3.6 ExceptionWaitStrategy 异常等待策略
withWaitStrategy(WaitStrategies.exceptionWait(ArithmeticException.class, e -> 1000L))

根据所发生的异常指定重试的等待时长;如果异常不匹配,则等待时长为0;


3.7 CompositeWaitStrategy 复合等待策略
.withWaitStrategy(WaitStrategies.join(WaitStrategies.exceptionWait(ArithmeticException.class, e -> 1000L),WaitStrategies.fixedWait(5, TimeUnit.SECONDS)))

复合等待策略;如果所执行的程序满足一个或多个等待策略,那么等待时间为所有等待策略时间的总和。

4. StopStrategies 重试停止策略
4.1 NeverStopStrategy
withStopStrategy(StopStrategies.neverStop())

一直不停止,一直需要重试。


4.2 StopAfterAttemptStrategy
withStopStrategy(StopStrategies.stopAfterAttempt(3))

在重试次数达到最大次数之后,终止任务。


4.3 StopAfterDelayStrategy
withStopStrategy(StopStrategies.stopAfterDelay(3, TimeUnit.MINUTES))

在重试任务达到设置的最长时长之后,无论任务执行次数,都终止任务。

5. BlockStrategies 阻塞策略

阻塞策略默认提供的只有一种:ThreadSleepStrategy,实现方式是通过Thread.sleep(sleepTime)来实现;不过这也给了我们极大的发挥空间,我们可以自己实现阻塞策略。

6. AttemptTimeLimiters 任务执行时长限制

这个表示单次任务执行时间限制(如果单次任务执行超时,则终止执行当前任务);

6.1 NoAttemptTimeLimit 无时长限制
.withAttemptTimeLimiter(AttemptTimeLimiters.noTimeLimit())

顾名思义,不限制执行时长;每次都是等执行任务执行完成之后,才进行后续的重试策咯。


6.2 FixedAttemptTimeLimit
.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(10, TimeUnit.SECONDS));
.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(10, TimeUnit.SECONDS, Executors.newCachedThreadPool()));

可以指定任务的执行时长限制,并且为了控制线程管理,最好指定相应的线程池。

四、总结

  1. guava-retrying功能强大,基本能满足我们常用的操作;如果不满足当前各种已有的策咯,可以选择分别继承WaitStrategyStopStrategyBlockStrategy来自定义自己的实现;
  2. guava-retrying默认的阻塞策咯是通过Thread.sleep来实现的,也就是说通过让当前线程休眠来实现阻塞功能,这或许不是一种很好的选择;
  3. 在实际使用过程种,我们可能会经常要调整重试次数、重试时间等策咯,所以我们可以将重试策咯的配置进行参数化保存,达到动态调节的目的;另外在使用的时候,也可以封装成util工具类供大家使用;

本文参考:
重试利器之Guava Retrying
https://github.com/rholder/guava-retrying

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容