Java 中的重试模式:使用自适应重试构建容错系统
也称为
- 重试逻辑
- 重试机制
重试设计模式的意图
Java 中的重试模式透明地重试某些涉及与外部资源(尤其是网络上的资源)通信的操作,将调用代码与重试实现细节隔离开来。对于开发能够优雅地处理瞬态错误的弹性软件系统至关重要。
重试模式的详细解释及实际示例
实际示例
想象一下,您是一名送货员,试图将包裹送到客户家中。您按了门铃,但没有人应答。您并没有立即离开,而是等待了几分钟并再次尝试,重复此过程几次。这类似于软件中的重试模式,其中系统会尝试重新执行失败的操作(例如,发出网络请求)一定次数,然后才最终放弃,希望问题(例如,瞬态网络故障)会解决并且操作会成功。
通俗地说
重试模式透明地重试网络上的失败操作。
Microsoft 文档 中说
通过透明地重试失败的操作,使应用程序能够处理尝试连接到服务或网络资源时的瞬态错误。这可以提高应用程序的稳定性。
Java 中重试模式的编程示例
重试设计模式是一种弹性模式,允许应用程序透明地尝试多次执行操作,以期成功。这种模式在应用程序连接到网络服务或远程资源时特别有用,因为在这种情况下,临时错误很常见。
首先,我们有一个 BusinessOperation
接口,它表示可以执行的操作,并且可能会抛出 BusinessException
。
public interface BusinessOperation<T> {
T perform() throws BusinessException;
}
接下来,我们有一个 FindCustomer
类,它实现了此接口。此类模拟了一个不稳定的服务,它在最终返回客户 ID 之前,会间歇性地通过抛出 BusinessException
来失败。
public final class FindCustomer implements BusinessOperation<String> {
@Override
public String perform() throws BusinessException {
// ...
}
}
Retry
类是实现重试模式的地方。它接受一个 BusinessOperation
和尝试次数,它会不断尝试执行操作,直到它成功或达到最大尝试次数为止。
public final class Retry<T> implements BusinessOperation<T> {
private final BusinessOperation<T> op;
private final int maxAttempts;
private final long delay;
private final AtomicInteger attempts;
private final Predicate<Exception> test;
private final List<Exception> errors;
@SafeVarargs
public Retry(
BusinessOperation<T> op,
int maxAttempts,
long delay,
Predicate<Exception>... ignoreTests
) {
this.op = op;
this.maxAttempts = maxAttempts;
this.delay = delay;
this.attempts = new AtomicInteger();
this.test = Arrays.stream(ignoreTests).reduce(Predicate::or).orElse(e -> false);
this.errors = new ArrayList<>();
}
public List<Exception> errors() {
return Collections.unmodifiableList(this.errors);
}
public int attempts() {
return this.attempts.intValue();
}
@Override
public T perform() throws BusinessException {
do {
try {
return this.op.perform();
} catch (BusinessException e) {
this.errors.add(e);
if (this.attempts.incrementAndGet() >= this.maxAttempts || !this.test.test(e)) {
throw e;
}
try {
Thread.sleep(this.delay);
} catch (InterruptedException f) {
//ignore
}
}
} while (true);
}
}
在此类中,perform
方法尝试执行操作。如果操作抛出异常,它会检查异常是否可恢复以及是否已达到最大尝试次数。如果这两个条件都为真,它会等待指定的延迟时间,然后再次尝试。如果异常不可恢复或已达到最大尝试次数,它会重新抛出异常。
最后,这是驱动重试模式示例的 App
类。
public final class App {
private static final Logger LOG = LoggerFactory.getLogger(App.class);
public static final String NOT_FOUND = "not found";
private static BusinessOperation<String> op;
public static void main(String[] args) throws Exception {
noErrors();
errorNoRetry();
errorWithRetry();
errorWithRetryExponentialBackoff();
}
private static void noErrors() throws Exception {
op = new FindCustomer("123");
op.perform();
LOG.info("Sometimes the operation executes with no errors.");
}
private static void errorNoRetry() throws Exception {
op = new FindCustomer("123", new CustomerNotFoundException(NOT_FOUND));
try {
op.perform();
} catch (CustomerNotFoundException e) {
LOG.info("Yet the operation will throw an error every once in a while.");
}
}
private static void errorWithRetry() throws Exception {
final var retry = new Retry<>(
new FindCustomer("123", new CustomerNotFoundException(NOT_FOUND)),
3, //3 attempts
100, //100 ms delay between attempts
e -> CustomerNotFoundException.class.isAssignableFrom(e.getClass())
);
op = retry;
final var customerId = op.perform();
LOG.info(String.format(
"However, retrying the operation while ignoring a recoverable error will eventually yield "
+ "the result %s after a number of attempts %s", customerId, retry.attempts()
));
}
private static void errorWithRetryExponentialBackoff() throws Exception {
final var retry = new RetryExponentialBackoff<>(
new FindCustomer("123", new CustomerNotFoundException(NOT_FOUND)),
6, //6 attempts
30000, //30 s max delay between attempts
e -> CustomerNotFoundException.class.isAssignableFrom(e.getClass())
);
op = retry;
final var customerId = op.perform();
LOG.info(String.format(
"However, retrying the operation while ignoring a recoverable error will eventually yield "
+ "the result %s after a number of attempts %s", customerId, retry.attempts()
));
}
}
运行代码将产生以下控制台输出。
10:12:19.573 [main] INFO com.iluwatar.retry.App -- Sometimes the operation executes with no errors.
10:12:19.575 [main] INFO com.iluwatar.retry.App -- Yet the operation will throw an error every once in a while.
10:12:19.682 [main] INFO com.iluwatar.retry.App -- However, retrying the operation while ignoring a recoverable error will eventually yield the result 123 after a number of attempts 1
10:12:22.297 [main] INFO com.iluwatar.retry.App -- However, retrying the operation while ignoring a recoverable error will eventually yield the result 123 after a number of attempts 1
这样,重试模式允许应用程序优雅地处理临时错误,从而提高其弹性和可靠性。
何时在 Java 中使用重试模式
应用重试模式特别有效
- 当操作可能会瞬态失败时,例如网络调用、数据库连接或外部服务集成。
- 在瞬态失败的可能性很高但重试成本较低的情况下。
Java 中重试模式的实际应用
- 在网络通信库中处理瞬态错误。
- 数据库连接库管理临时中断或超时。
- 与可能暂时不可用的第三方服务交互的 API。
重试模式的优缺点
优点
- 提高应用程序的健壮性和容错能力。
- 可以显著减少瞬态错误的影响。
权衡
- 由于重试可能会导致延迟。
- 如果管理不当,会导致资源耗尽。
- 需要仔细配置重试参数,以避免加剧问题。
相关的 Java 设计模式
- 断路器:用于在达到失败阈值后停止对外部服务的请求流,防止系统过载。