优雅编程 - 重试组件

2019-12-19  本文已影响0人  林昀熙

业务开发中为了保证衔接模块的偶尔不确定性,需要做一些重试保障机制. 为了让我们的重试代码更优雅简单, 这里介绍两个方案:Guava-RetrySpring-Retry

Guava-Retrying

Guava Retrying 是一个灵活方便的重试组件,包含了多种的重试策略,而且扩展起来非常容易.

maven依赖

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

应用示例

示例展示目标接口在返回true时进行逻辑重试, 重试次数为3次, 重试时间间隔为每间隔2s执行一次重试.

public static void main(String[] args) {
    Retryer<Boolean> retryer = RetryerBuilder.<Boolean> newBuilder()
            .retryIfException()
            .retryIfResult(Predicates.equalTo(true))
            .withBlockStrategy(BlockStrategies.threadSleepStrategy())
            .withStopStrategy(StopStrategies.stopAfterAttempt(3))
            .withWaitStrategy(WaitStrategies.fixedWait(2, TimeUnit.SECONDS))
            .build();
    boolean withRetry = delayRetry(retryer);
    System.out.println("[RETRY-RESULT]: " + withRetry);
}

public static boolean delayRetry(Retryer<Boolean> retryer){
    boolean result = false;
    try {
        result = retryer.call(new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                try {
                    System.out.println("[TIME-STAMP]:" + System.currentTimeMillis());
                    return true;
                } catch (Exception e) {
                    throw new Exception(e);
                }
            }
        });
    } catch (Exception e) {
        log.error(e.getLocalizedMessage(), e);
    }
    return result;
}

示例输出

[TIME-STAMP]:1521125204339
[TIME-STAMP]:1521125206340
[TIME-STAMP]:1521125208342
[RETRY-RESULT]: false

代码解读

RetryerBuilder用于构造重试实例, 用于设置重试源(可以支持多个重试源)、重试次数、重试超时时间以及等待时间间隔等.

策略说明

任务阻塞策略 (BlockStrategies)

通俗的讲就是当前任务执行完,下次任务还没开始这段时间做什么, 默认策略为 BlockStrategies.THREAD_SLEEP_STRATEGY 也就是调用 Thread.sleep(sleepTime).

停止重试策略 (StopStrategy)
重试间隔策略 (WaitStrategies)

代码示例: 异常直接重试4次

Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
                .retryIfException().retryIfResult(Predicates.equalTo(true))
                .withBlockStrategy(BlockStrategies.threadSleepStrategy())
                .withStopStrategy(StopStrategies.stopAfterAttempt(4))
                .withWaitStrategy(WaitStrategies.noWait())
                .build();

执行输出:

[TIME-STAMP]:1521196411381
[TIME-STAMP]:1521196411381
[TIME-STAMP]:1521196411381
[TIME-STAMP]:1521196411381
[RETRY-RESULT]: false

代码示例: 每隔2秒执行一次重试

 Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
                .retryIfException().retryIfResult(Predicates.equalTo(true))
                .withBlockStrategy(BlockStrategies.threadSleepStrategy())
                .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                .withWaitStrategy(WaitStrategies.fixedWait(2, TimeUnit.SECONDS))
                .build();

执行输出

[TIME-STAMP]:1521195170114
[TIME-STAMP]:1521195172119
[TIME-STAMP]:1521195174122
[RETRY-RESULT]: false

代码示例: 随机间隔0~2秒重试

 Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
                .retryIfException().retryIfResult(Predicates.equalTo(true))
                .withBlockStrategy(BlockStrategies.threadSleepStrategy())
                .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                .withWaitStrategy(WaitStrategies.randomWait(2, TimeUnit.SECONDS))
                .build();

执行输出:

[TIME-STAMP]:1521601715654
[TIME-STAMP]:1521601717496
[TIME-STAMP]:1521601718763
[RETRY-RESULT]: false

代码示例: 首次间隔1s,以后每次增加3s重试. 时间维度为: initialSleepTime + increment * (attemptNumber - 1)

Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
                .retryIfException().retryIfResult(Predicates.equalTo(true))
                .withBlockStrategy(BlockStrategies.threadSleepStrategy())
                .withStopStrategy(StopStrategies.stopAfterAttempt(4))
                // initialSleepTime:第一次到第二次尝试的间隔, increment: 每增加一次尝试,需要增加的时间间隔
                .withWaitStrategy(WaitStrategies.incrementingWait(1, TimeUnit.SECONDS, 3, TimeUnit.SECONDS))
                .build();
[TIME-STAMP]:1521194872168
[TIME-STAMP]:1521194873172
[TIME-STAMP]:1521194877173
[TIME-STAMP]:1521194884175
[RETRY-RESULT]: false

fibonacciWait(long multiplier,long maximumTime,TimeUnit maximumTimeUnit); multiplier单位固定是ms, maximumTime最大等待时间.

代码示例: 采用斐波那契数列时长进行重试

Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
                .retryIfException().retryIfResult(Predicates.equalTo(true))
                .withBlockStrategy(BlockStrategies.threadSleepStrategy())
                .withStopStrategy(StopStrategies.stopAfterAttempt(4))
                // 斐波那契数列
                .withWaitStrategy(WaitStrategies.fibonacciWait(5, TimeUnit.SECONDS))
                .build();

执行输出

[TIME-STAMP]:1521195661799
[TIME-STAMP]:1521195661801
[TIME-STAMP]:1521195661802
[TIME-STAMP]:1521195661805
[RETRY-RESULT]: false

代码示例

Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
                .retryIfException().retryIfResult(Predicates.equalTo(true))
                .withBlockStrategy(BlockStrategies.threadSleepStrategy())
                .withStopStrategy(StopStrategies.stopAfterAttempt(4))
                .withWaitStrategy(WaitStrategies.exponentialWait(100, 10, TimeUnit.SECONDS))
                .build();

执行输出

[TIME-STAMP]:1521196302323
[TIME-STAMP]:1521196302527
[TIME-STAMP]:1521196302932
[TIME-STAMP]:1521196303736
[RETRY-RESULT]: false

Spring-Retry

spring-retry非常简单,在配置类加上@EnableRetry注解启用spring-retry, 然后在需要失败重试的方法加@Retryable注解即可, spring-retry通过捕获异常来触发重试机制.

Maven依赖

<dependency>
  <groupId>org.springframework.retry</groupId>
  <artifactId>spring-retry</artifactId>
  <version>1.2.2.RELEASE</version>
</dependency>

应用示例

硬编码方式

示例代码

@Test
public void springRetry() throws Exception {

    // 构建重试模板实例
    RetryTemplate retryTemplate = new RetryTemplate();

    // 设置重试策略
    retryTemplate.setRetryPolicy(new SimpleRetryPolicy(3, Collections.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class, true)));

    // 设置退避策略
    FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
    fixedBackOffPolicy.setBackOffPeriod(100);
    retryTemplate.setBackOffPolicy(fixedBackOffPolicy);

    boolean withRetry = retryTemplate.execute(
            // 重试行为
            new RetryCallback<Boolean, Exception>() {

                @Override
                public Boolean doWithRetry(RetryContext retryContext) throws Exception {
                    System.out.println("[TIME-STAMP]:" + System.currentTimeMillis() + ", retry:" + retryContext.getRetryCount());
                    return sample(5);
                }
            },
            // 多次重试无效后执行逻辑
            new RecoveryCallback<Boolean>() {

                @Override
                public Boolean recover(RetryContext retryContext) throws Exception {
                    System.out.println("[TIME-STAMP]:" + System.currentTimeMillis() + ", recover:" + retryContext.getRetryCount());
                    return false;
                }
            }
    );
    System.out.println("[RETRY-RESULT]: " + withRetry);
}

private boolean sample(int id) throws Exception {
    if(id < 10){
        throw new RuntimeException(String.valueOf(id));
    }
    return true;
}

程序输出

[TIME-STAMP]:1521207239963, retry:0
[TIME-STAMP]:1521207240065, retry:1
[TIME-STAMP]:1521207240166, retry:2
[TIME-STAMP]:1521207240166, recover:3
[RETRY-RESULT]: false

上面示例中我们的sample()方法入参为5时输出结果如上图, 当参数设置大于10时输出结果如下.

[TIME-STAMP]:1521207481718, retry:0
[RETRY-RESULT]: true
重试策略
退避策略

如果每次有重试需求的时候都写一个RetryTemplate太臃肿了,SpringRetry也提供了使用注解方式进行重试操作.

注解方式
应用示例

示例代码

@Slf4j
@Service
@EnableRetry
public class SpringRetry {

    @Retryable(value = {Exception.class}, maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 1.5))
    public String withRetry(long id){
        System.out.println("[TIME-STAMP]:" + System.currentTimeMillis() + ", id=" + id);
        if(id < 10){
            throw new IllegalArgumentException(String.valueOf(id));
        }
        return String.valueOf(id);
    }

    @Recover
    public String withRecover(Exception exception, long id){
        System.out.println("[TIME-STAMP]:" + System.currentTimeMillis() + ", id=" + id + ", withRecover");
        return StringUtils.EMPTY;
    }

}

执行结果

[TIME-STAMP]:1521207829018, id=11

[TIME-STAMP]:1521207829018, id=8
[TIME-STAMP]:1521207831023, id=8
[TIME-STAMP]:1521207834027, id=8
[TIME-STAMP]:1521207834028, id=8, withRecover
注解说明

参考文档

上一篇下一篇

猜你喜欢

热点阅读