这重试器写的真地地地地地地道,你觉得地道吗?

开发 前端
我们利用CompletableFuture API完成相同的任务,同时保持异步流程。我们发现了Java 12中引入的​​exceptionallyAsync()​​方法,它非常适合这个目的。最后,我们提出了一种替代方法,仅依赖于原始Java 8 API中的方法。

服务总是不稳定的,有的时候需要编写重试逻辑,比如,HTTP的重试;定时任务的重试等。

一、简单的实现

我们可以使用whilefor循环,配置try-catchbreak组合,完成循环逻辑。

final Random random = new Random();

final int maxRetryCount = 10;
int times = 0;
while (true) {
    times++;
    if (times > maxRetryCount) {
        break;
    }
    try {
        // 业务逻辑
        System.out.println("最大重试" + maxRetryCount + "次,当前是第" + times + "次");
        if (random.nextInt(10) > 5) {
            throw new RuntimeException("随机数失败");
        }

        if (random.nextInt(10) / 2 == 0) {
            System.out.println("逻辑执行成功");
            break;
        }
        Thread.sleep(1000);
        // 业务逻辑
    } catch (Exception e) {
        System.out.println("进入异常捕获");
    }
}
System.out.println("业务逻辑执行完毕");

其中一次执行结果:

最大重试10次,当前是第1次 进入异常捕获 最大重试10次,当前是第2次 进入异常捕获 最大重试10次,当前是第3次 最大重试10次,当前是第4次 进入异常捕获 最大重试10次,当前是第5次 最大重试10次,当前是第6次 进入异常捕获 最大重试10次,当前是第7次 最大重试10次,当前是第8次 逻辑执行成功 业务逻辑执行完毕

上面这种实现算是重试逻辑的模板化代码,大差不差的都是这种写法。

我们再看看其他的写法。

二、重试装饰器的实现

本节我们使用装饰器模式实现,借助经典的面向对象编程风格(通过类和接口)。同时,我们选择更简洁的函数式方法。

首先,我们将声明一个函数,接收Supplier<T>和最大调用次数作为参数。

然后,还是使用while循环和try-catch块多次调用该函数。

最后,我们将通过返回另一个Supplier<T>来保留原始数据类型。

static <T> Supplier<T> retryFunction(Supplier<T> supplier, int maxRetries) {
    return () -> {
        int retries = 0;
        while (retries < maxRetries) {
            try {
                return supplier.get();
            } catch (Exception e) {
                retries++;
            }
        }
        throw new IllegalStateException(String.format("任务在 %s 次尝试后失败", maxRetries));
    };
}

有了上面的函数,我们可以基于这个函数装饰器继续创建CompletableFuture

static <T> CompletableFuture<T> retryTask(Supplier<T> supplier, int maxRetries) {
    Supplier<T> retryableSupplier = retryFunction(supplier, maxRetries);
    return CompletableFuture.supplyAsync(retryableSupplier);
}

模拟下业务场景,有个标志位数字,我们需要检查这个标志位是否大于4,如果大于就结束任务,如果小于4,重试。

final AtomicInteger retriesCounter = new AtomicInteger(0);

Supplier<Integer> codeToRun = () -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
    int retryNr = retriesCounter.get();
    System.out.println("Retrying: " + retryNr + "; thread:" + Thread.currentThread().getName());
    if (retryNr < 4) {
        retriesCounter.incrementAndGet();
        throw new RuntimeException();
    }
    return 100;
};

然后我们借助retryTask()函数完成重试逻辑,假设最大重试10次,根据上面的定义,正常会在第四次跳出:

CompletableFuture<Integer> result = retryTask(codeToRun, 10);

结果会打印:

Retrying: 0 Retrying: 1 Retrying: 2 Retrying: 3 Retrying: 4 100 4

如果重试次数小于4,就会触发IllegalStateException异常:

try {
    result = retryTask(codeToRun, 3);
    System.out.println(result.get());
} catch (Exception e) {
    System.out.println("超过最大重试次数");
}

结果会打印:

Retrying: 0 Retrying: 1 Retrying: 2 超过最大重试次数

三、重试CompletableFuture

CompletableFuture提供了内部逻辑出现异常时处理的方法,比如exceptionally()等方法,我们可以直接使用这些方法,不需要自定义装饰器。

CompletableFuture的使用可以参考由浅入深掌握CompletableFuture的七种用法

(一)不安全重试

exceptionally()方法允许指定一个替代函数,当主要逻辑出现异常时,会调用指定的替代函数。

如果我们打算重试两次,我们可以这样写:

static <T> CompletableFuture<T> retryTwice(Supplier<T> supplier) {
    return CompletableFuture.supplyAsync(supplier)
     .exceptionally(__ -> supplier.get())
     .exceptionally(__ -> supplier.get());
}

如果重试次数可变,我们可以使用for循环:

static <T> CompletableFuture<T> retryUnsafe(Supplier<T> supplier, int maxRetries) {
    CompletableFuture<T> cf = CompletableFuture.supplyAsync(supplier);
    for (int i = 0; i < maxRetries; i++) {
        cf = cf.exceptionally(__ -> supplier.get());
    }
    return cf;
}

上面的写法可以满足需求,但是有一点需要注意,当Supplier运行比较快,在CompletableFutureexceptionally()回退创建之前执行完毕,那exceptionally()的方法就会在主线程执行。比如,我们设定retryUnsafe休眠1000ms,指定codeToRun不做sleep:

static <T> CompletableFuture<T> retryUnsafe(Supplier<T> supplier, int maxRetries) throws InterruptedException {
    CompletableFuture<T> cf = CompletableFuture.supplyAsync(supplier);
    Thread.sleep(1000);
    for (int i = 0; i < maxRetries; i++) {
        cf = cf.exceptionally(__ -> supplier.get());
    }
    return cf;
}

codeToRun = () -> {
    int retryNr = retriesCounter.get();
    System.out.println("Retrying: " + retryNr + "; thread:" + Thread.currentThread().getName());
    if (retryNr < 4) {
        retriesCounter.incrementAndGet();
        throw new RuntimeException();
    }
    return 100;
};
retryUnsafe(codeToRun, 3);

运行结果将是:

Retrying: 0; thread:ForkJoinPool.commonPool-worker-1 Retrying: 1; thread:main Retrying: 2; thread:main Retrying: 3; thread:main

符合预期,后续调用是由主线程执行的。如果初始调用很快,但后续调用预计会更慢,这可能会成为问题。

(二)异步重试

如果是在Java12之后,我们可以通过exceptionallyAsync()方法实现,将所有重试都异步执行。

static <T> CompletableFuture<T> retryExceptionallyAsync(Supplier<T> supplier, int maxRetries) {
   CompletableFuture<T> cf = CompletableFuture.supplyAsync(supplier);
   for (int i = 0; i < maxRetries; i++) {
      cf = cf.exceptionallyAsync(__ -> supplier.get());
   }
   return cf;
}

这个时候运行结果将是:

Retrying: 0; thread:ForkJoinPool.commonPool-worker-1
Retrying: 1; thread:ForkJoinPool.commonPool-worker-1
Retrying: 2; thread:ForkJoinPool.commonPool-worker-1
Retrying: 3; thread:ForkJoinPool.commonPool-worker-1

(三)嵌套CompletableFutures

如果是在Java12之前,我们需要实现兼容方案,那我们可以写一个增加逻辑,实现异步化,我们可以这样写:

static <T> CompletableFuture<T> retryNesting(Supplier<T> supplier, int maxRetries)
        throws InterruptedException {
    CompletableFuture<T> cf = CompletableFuture.supplyAsync(supplier);
    Thread.sleep(1000);
    for (int i = 0; i < maxRetries; i++) {
        cf = cf.thenApply(CompletableFuture::completedFuture)
                .exceptionally(__ -> CompletableFuture.supplyAsync(supplier))
                .thenCompose(Function.identity());
    }
    return cf;
}

其实简单或就是有创建了一个CompletableFuture,运行结果也是符合预期的。

四、总结

在本文中,我们探讨了在CompletableFuture中重试函数调用的概念。我们首先深入研究了以函数式风格实现装饰器模式,使我们能够重试函数本身。

随后,我们利用CompletableFuture API完成相同的任务,同时保持异步流程。我们发现了Java 12中引入的exceptionallyAsync()方法,它非常适合这个目的。最后,我们提出了一种替代方法,仅依赖于原始Java 8 API中的方法。

文末总结

本文介绍了使用实现重试逻辑的三种方式:简单模式、装饰器模式、和利用CompletableFuture原生API的实现。

责任编辑:武晓燕 来源: 看山的小屋
相关推荐

2021-01-22 11:35:19

物联网人工智能编程

2016-03-01 17:48:32

WLAN控制器网络管理

2022-09-20 07:46:15

重试组件retrying

2021-09-08 08:34:37

Go 文档Goland

2013-04-16 10:34:57

敏捷

2021-03-08 07:46:53

Git开源控制系统

2009-07-29 17:12:56

光纤入户光纤桌面

2023-11-28 08:22:05

goroutine语言

2012-05-10 10:20:04

memcached缓存

2017-03-19 20:41:57

WebAssemblyJavaScript编程

2019-10-21 09:57:05

多云混合云云计算

2019-03-22 14:20:26

管理多云云计算

2021-03-24 10:20:50

Fonts前端代码

2020-06-29 18:54:39

大数据新发地疫情

2014-12-03 10:27:06

BYODBYOD安全移动设备

2022-04-19 07:51:11

RPC 通信架构

2014-10-29 09:55:58

程序员代码

2016-10-14 06:53:29

网络安全生态

2012-07-16 09:10:35

2021-04-13 07:02:15

VRAR虚拟现实技术
点赞
收藏

51CTO技术栈公众号