“重试”失败逻辑的设计模式是什么?

73

我正在编写一些重新连接逻辑,以周期性地尝试与远程端点建立连接(该端点已经关闭)。基本上,代码看起来像这样:

public void establishConnection() {
    try {
        this.connection = newConnection();
    } catch (IOException e) {
        // connection failed, try again.
        try { Thread.sleep(1000); } catch (InterruptedException e) {};

        establishConnection();
    }
}

我已经多次使用类似上面的代码解决了这个普遍问题,但我对结果感到大部分不满意。是否有专为处理此问题而设计的设计模式?


1
像尝试三次然后提高标志模式一样? :) - Nishant
在这种情况下,我倾向于使用循环而不是递归——否则,如果它一直停留在那里,你最终会得到一个巨大的堆栈。这也会使在一定数量的重试后容易退出(例如,如果您使用for循环,则已经内置了此机制)。 - Michael Berry
你能捕获“断开连接”事件吗? - Shark
1
这里有一些不错的答案。需要注意的一点是(这里没有提到),通常最好实现某种退避策略(在连续重试之间等待更长时间)。这将避免对未响应的服务器进行过度访问(可能由于负载问题)。 - jtahlborn
@jtahlborn:这是我解决方案中可用的等待策略之一 :-) - JB Nizet
12个回答

33

不好意思打广告了: 我已经实现了一些类以允许重新尝试操作。 该库尚未公开,但您可以在GitHub上fork它。同时,另一个分支也存在。

这允许构建带有各种灵活策略的Retryer。 例如:

Retryer retryer = 
    RetryerBuilder.newBuilder()
                  .withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECOND))
                  .withStopStrategy(StopStrategies.stopAfterAttempt(3))
                  .retryIfExceptionOfType(IOException.class)
                  .build();

然后您可以使用Retryer执行可调用函数(或多个函数):

retryer.call(new Callable<Void>() {
    public Void call() throws IOException {
        connection = newConnection();
        return null;
    }
}

我曾经在工作中构建过类似的东西,但使用了装饰器模式而不是建造者模式。这两种方式都能完成工作,并且是非常优雅的重试方法。它允许你编写一个简单的Callable实现,而无需自己编写重试逻辑。 - Matt
实际上,构建器只是用于配置不可变的重试器。重试器可以用来将一个 Callable 包装成另一个 Callable。因此它也使用了装饰器模式。 - JB Nizet
@JBNizet 感谢您提供的代码片段,但是在 withStopStrategy(StopStrategies.stopAfterAttempt(3)) 行上出现了错误,错误信息为 The method withStopStrategy(StopStrategy) is undefined for the type WaitStrategy。这里是我正在使用的整行代码:Retryer retryer = RetryerBuilder.newBuilder().withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS).withStopStrategy(StopStrategies.stopAfterAttempt(5)).retryIfExceptionOfType(IOException.class).build()); - Bhargav Modi
2
你(和我)忘记了关闭一个括号:withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS)) <-- 这里有两个括号 - JB Nizet
1
我从上述作者的代码中fork了一个项目,运气非常好:https://github.com/rholder/guava-retrying - jstricker
@JBNizet 我在我的一个项目中也使用了guava retry,但我对线程安全的使用感到困惑,所以我有一个问题在这里,我想知道你是否能帮助我? - john

30

17

我非常喜欢这篇来自这个博客的Java 8代码,而且你不需要在类路径上添加任何额外的库。

你只需要将一个函数传递给重试类即可。

@Slf4j
public class RetryCommand<T> {

    private int maxRetries;

    RetryCommand(int maxRetries)
    {
        this.maxRetries = maxRetries;
    }

    // Takes a function and executes it, if fails, passes the function to the retry command
    public T run(Supplier<T> function) {
        try {
            return function.get();
        } catch (Exception e) {
            log.error("FAILED - Command failed, will be retried " + maxRetries + " times.");
            return retry(function);
        }
    }

    private T retry(Supplier<T> function) throws RuntimeException {

        int retryCounter = 0;
        while (retryCounter < maxRetries) {
            try {
                return function.get();
            } catch (Exception ex) {
                retryCounter++;
                log.error("FAILED - Command failed on retry " + retryCounter + " of " + maxRetries, ex);
                if (retryCounter >= maxRetries) {
                    log.error("Max retries exceeded.");
                    break;
                }
            }
        }
        throw new RuntimeException("Command failed on all of " + maxRetries + " retries");
    }
}

使用它:

new RetryCommand<>(5).run(() -> client.getThatThing(id));

1
我喜欢那种方法,但它不能处理已检查异常。 - tristobal
在没有线性/指数延迟和随机抖动的情况下重试外部系统可能会过载目标服务,特别是如果它由于过度负载而经历资源匮乏。 - Ashwin Prabhu
1
@tristobal,你是说它不能处理已检查异常吗?看起来作者在那里捕获了所有的异常并重新尝试。或者你担心它不会将已检查异常传播到调用者下游? - Zhenya

13

使用Failsafe(作者在此):

RetryPolicy retryPolicy = new RetryPolicy()
  .retryOn(IOException.class)
  .withMaxRetries(5)
  .withDelay(1, TimeUnit.SECONDS);

Failsafe.with(retryPolicy).run(() -> newConnection());

没有注释,没有神奇的操作,也不需要成为一个Spring应用程序等等。只是直截了当和简单。


12

我正在使用AOP和Java注解。在jcabi-aspects(我是一名开发人员)中有一个现成的机制:

@RetryOnFailure(attempts = 3, delay = 1, unit = TimeUnit.SECONDS)
public void establishConnection() {
  this.connection = newConnection();
}

提示:你也可以尝试来自CactoosRetryScalar


我一直想创建类似的东西!我会尝试做出贡献!不过为什么不选择Apache许可证2.0呢? - Marsellus Wallace
@yegor256 我在哪里可以下载这个jar包? - Will
我知道这是一个旧的帖子,但我正在尝试使用jcabi-aspects,因为我喜欢注释的清晰界面。但是我遇到了麻烦,因为SonaType / Nexus在所需依赖项中标记了许多关键漏洞。有没有可能更新它以克服这些问题? - jwh20

7
你可以尝试使用 spring-retry,它具有简洁的接口设计,易于使用。
示例:
 @Retryable(maxAttempts = 4, backoff = @Backoff(delay = 500))
 public void establishConnection() {
    this.connection = newConnection();
 } 

如果发生异常,它会使用500毫秒的退避策略重试(调用)高达4次 establishConnection() 方法。


1
据我所知,@Retryable 只能在 Spring 应用程序中使用。https://www.baeldung.com/spring-retry - Valeriy K.

3

你还可以创建一个包装函数,只需要循环执行目标操作,当完成后就退出循环。

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

private static void retryMySpecialOperation(int retries) {
    for (int i = 1; i <= retries; i++) {
        try {
            specialOperation();
            break;
        }
        catch (Exception e) {
            System.out.println(String.format("Failed operation. Retry %d", i));
        }
    }
}

private static void specialOperation() throws Exception {
    if ((int) (Math.random()*100) % 2 == 0) {
        throw new Exception("Operation failed");
    }
    System.out.println("Operation successful");
}

2

如果您正在使用Java 8,这可能会有所帮助。

import java.util.function.Supplier;

public class Retrier {
public static <T> Object retry(Supplier<T> function, int retryCount) throws Exception {
     while (0<retryCount) {
        try {
            return function.get();
        } catch (Exception e) {
            retryCount--;
            if(retryCount == 0) {
                throw e;
            }
        }
    }
    return null;
}

public static void main(String[] args) {
    try {
        retry(()-> {
            System.out.println(5/0);
            return null;
        }, 5);
    } catch (Exception e) {
        System.out.println("Exception : " + e.getMessage());
    }
}
}

谢谢, Praveen R.

2
我将使用retry4j库。下面是测试代码示例:
public static void main(String[] args) {
    Callable<Object> callable = () -> {
        doSomething();
        return null;
    };

    RetryConfig config = new RetryConfigBuilder()
            .retryOnAnyException()
            .withMaxNumberOfTries(3)
            .withDelayBetweenTries(5, ChronoUnit.SECONDS)
            .withExponentialBackoff()
            .build();

    new CallExecutorBuilder<>().config(config).build().execute(callable);
}

public static void doSomething() {
    System.out.println("Trying to connect");
    // some logic
    throw new RuntimeException("Disconnected"); // init error
    // some logic
}

1
这里有另一种执行重试的方法。没有库、注解或额外的实现。导入java.util.concurrent.TimeUnit;
public static void myTestFunc() {
        boolean retry = true;
        int maxRetries = 5;   //max no. of retries to be made
        int retries = 1;
        int delayBetweenRetries = 5;  // duration  between each retry (in seconds)
        int wait = 1;
    do {
        try {
            this.connection = newConnection();
            break;
        }
        catch (Exception e) {
            wait = retries * delayBetweenRetries;
            pause(wait);
            retries += 1;
            if (retries > maxRetries) {
                retry = false;
                log.error("Task failed on all of " + maxRetries + " retries");
            }
        }
    } while (retry);

}

public static void pause(int seconds) {
    long secondsVal = TimeUnit.MILLISECONDS.toMillis(seconds);

    try {
        Thread.sleep(secondsVal);
    }
    catch (InterruptedException ex) {
        Thread.currentThread().interrupt();
    }
}

}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接