函数式编程牛刀小试-更优雅地进行接口重试调用

本文最后更新于:2021年6月15日 晚上

背景是项目属于微服务架构,项目之间无法避免地需要进行接口调用,公司使用的是内部开发的 RPC 框架,框架不支持重试机制。

在日常的业务开发当中,自己的服务系统往往依赖多个其他服务,但是无法保证依赖服务的稳定性,如果出现依赖服务短暂不可用就会导致自己的服务出现问题。

这种问题很烦,但是又无可避免,只能采取其他的途径尽量减少这种问题的发生,而接口调用重试便是最简单的方式。

最近在重构自己以前写的代码,依赖接口调用的重试是通过递归来实现的,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public User getUser(Long uid, int retryCount) {
User info = new User();
int ret = userClient.getUser(uid, info);
if (RetCode.RET_SUCCESS == ret) {
return info;
}

if (retryCount > 0) {
sleep3ms();
return getUser(uid, --retryCount);
}
log.error("getUser fail. ret: {}, uid: {}", ret, uid);
throw new ApiException("获取用户信息失败");
}

public OrgInfo getOrg(Long orgId, int retryCount) {
OrgInfo info = new OrgInfo();
int ret = orgClient.getOrg(orgId, info);
if (RetCode.RET_SUCCESS == ret) {
return info;
}

if (retryCount > 0) {
sleep3ms();
return getOrg(orgId, --retryCount);
}
log.error("getOrg fail. ret: {}, orgId: {}", ret, orgId);
throw new ApiException("获取企业信息失败");
}

private void sleep3ms() {
try {
Thread.sleep(3);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}

调用方法方式:getUser(uid, 3) getOrg(orgId, 3) 当依赖接口短暂出现问题,就进行递归重试,重试成功就返回调用结果;当重试次数全部用完,直接抛出异常,这种情况一般是对方服务挂掉了,强依赖的场景下,自己的业务也是无法进行的。

这两个方法很明显的问题就是,方法高度类似却又不同。

正好最近在看 CompletableFuture 源码,源码中对于 Java8 中的函数式接口使用给予了我启发,这段重试的代码可以通过函数式编程的方式来进行优化。

首先需要了解几个基础的函数式接口:

  • Function<T, R> :接收一个参数,并返回一个值
  • Supplier<T>:不接收参数,返回一个值
  • Predicate<T>:谓词,接收一个参数,返回一个Boolean结果

函数式编程的特点就是将函数的实现放到调用者处,而不是函数内部,传递行为而不是传递值,提供了更高层次的抽象。

大概理解这几个函数式接口的使用后,于是提炼出了如下工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
* 重试工具类
* 应用场景:接口调用重试
*
* @author WeJan
* @since 2021-02-06
*/
public class RetryUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(RetryUtil.class);


/**
* 方法重试调用(未执行成功才进行重试)
* 不处理方法调用异常
*
* @param invokeMethod 执行方法
* @param retryCondition 重试条件
* @param fixedWaitMilliseconds 重试间隔时间(毫秒)
* @param retryCount 重试次数
* @param exception 达到最大重试次数,抛出指定异常
* @return 方法返回
*/
public static <U> U invoke(Supplier<U> invokeMethod,
Predicate<U> retryCondition,
long fixedWaitMilliseconds,
int retryCount,
Supplier<? extends RuntimeException> exception) {
return RetryUtil.invoke(invokeMethod, retryCondition, fixedWaitMilliseconds, retryCount, null, exception);
}

/**
* 方法重试调用(未执行成功才进行重试)
* 可手动处理异常,设置默认值
* @param invokeMethod 执行方法
* @param retryCondition 重试条件
* @param fixedWaitMilliseconds 重试间隔时间(毫秒)
* @param retryCount 重试次数
* @param exceptionally 执行方法发生异常处理,返回默认值, 为 null 则直接上抛异常
* @param exception 达到最大重试次数,抛出指定异常
* @return 方法返回
*/
public static <U> U invoke(Supplier<U> invokeMethod,
Predicate<U> retryCondition,
long fixedWaitMilliseconds,
int retryCount,
Function<Exception, U> exceptionally,
Supplier<? extends RuntimeException> exception) {
if (invokeMethod == null || retryCondition == null || exception == null) {
throw new NullPointerException();
}
U result = null;
try {
result = invokeMethod.get();
} catch (Exception e) {
LOGGER.error("", e);
if (exceptionally != null) {
return exceptionally.apply(e);
} else {
throw e;
}
}
if (!retryCondition.test(result)) {
return result;
}
if (retryCount > 0) {
try {
TimeUnit.MILLISECONDS.sleep(fixedWaitMilliseconds);
} catch (InterruptedException ignored) {
}
LOGGER.error("retryCount: {}", retryCount);
return invoke(invokeMethod, retryCondition, fixedWaitMilliseconds, --retryCount, exceptionally, exception);
}
throw exception.get();
}

}

有了这个工具类,上面示例中的代码就可以修改为如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public User getUser(Long uid) {
return RetryUtil.invoke(() -> {
User info = new User();
int ret = userClient.getUser(uid, info);
if (RetCode.RET_SUCCESS == ret) {
return info;
}
return null;
},
Objects::isNull, // 重试条件为调用返回结果为null
5, // 等待5毫秒
3, // 重试3次
() -> new ApiException("获取用户信息失败")); // 超出重试次数抛出异常
}

public OrgInfo getOrg(Long orgId) {
return RetryUtil.invoke(() -> {
OrgInfo info = new OrgInfo();
int ret = orgClient.getOrg(orgId, info);
if (RetCode.RET_SUCCESS == ret) {
return info;
}
return null;
},
Objects::isNull,
5,
3,
() -> new ApiException("获取企业信息失败"));
}

这个工具类也算是自己对于函数式编程的第一次实践,发现还是蛮有意思的哈哈

成功水了一篇~


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!