链接: https://nullpointer.pw/retry-method-invoke.html
背景是项目属于微服务架构,项目之间无法避免地需要进行接口调用,公司使用的是内部开发的 RPC 框架,框架不支持重试机制。
在日常的业务开发当中,自己的服务系统往往依赖多个其他服务,但是无法保证依赖服务的稳定性,如果出现依赖服务短暂不可用就会导致自己的服务出现问题。
这种问题很烦,但是又无可避免,只能采取其他的途径尽量减少这种问题的发生,而接口调用重试便是最简单的方式。
最近在重构自己以前写的代码,依赖接口调用的重试是通过递归来实现的,代码如下:
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
:接收一个参数,并返回一个值 - Supplier
:不接收参数,返回一个值 - Predicate
:谓词,接收一个参数,返回一个Boolean结果
函数式编程的特点就是将函数的实现放到调用者处,而不是函数内部,传递行为而不是传递值,提供了更高层次的抽象。
大概理解这几个函数式接口的使用后,于是提炼出了如下工具类:
/**
* 重试工具类
* 应用场景:接口调用重试
*
* @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();
}
}
有了这个工具类,上面示例中的代码就可以修改为如下:
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("获取企业信息失败"));
}