链接: 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, // 重试条件为调用返回结果为null5, // 等待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("获取企业信息失败"));}
