链接: https://nullpointer.pw/retry-method-invoke.html

    背景是项目属于微服务架构,项目之间无法避免地需要进行接口调用,公司使用的是内部开发的 RPC 框架,框架不支持重试机制。
    在日常的业务开发当中,自己的服务系统往往依赖多个其他服务,但是无法保证依赖服务的稳定性,如果出现依赖服务短暂不可用就会导致自己的服务出现问题。
    这种问题很烦,但是又无可避免,只能采取其他的途径尽量减少这种问题的发生,而接口调用重试便是最简单的方式。
    最近在重构自己以前写的代码,依赖接口调用的重试是通过递归来实现的,代码如下:

    1. public User getUser(Long uid, int retryCount) {
    2. User info = new User();
    3. int ret = userClient.getUser(uid, info);
    4. if (RetCode.RET_SUCCESS == ret) {
    5. return info;
    6. }
    7. if (retryCount > 0) {
    8. sleep3ms();
    9. return getUser(uid, --retryCount);
    10. }
    11. log.error("getUser fail. ret: {}, uid: {}", ret, uid);
    12. throw new ApiException("获取用户信息失败");
    13. }
    14. public OrgInfo getOrg(Long orgId, int retryCount) {
    15. OrgInfo info = new OrgInfo();
    16. int ret = orgClient.getOrg(orgId, info);
    17. if (RetCode.RET_SUCCESS == ret) {
    18. return info;
    19. }
    20. if (retryCount > 0) {
    21. sleep3ms();
    22. return getOrg(orgId, --retryCount);
    23. }
    24. log.error("getOrg fail. ret: {}, orgId: {}", ret, orgId);
    25. throw new ApiException("获取企业信息失败");
    26. }
    27. private void sleep3ms() {
    28. try {
    29. Thread.sleep(3);
    30. } catch (InterruptedException ignored) {
    31. Thread.currentThread().interrupt();
    32. }
    33. }

    调用方法方式:getUser(uid, 3) getOrg(orgId, 3) 当依赖接口短暂出现问题,就进行递归重试,重试成功就返回调用结果;当重试次数全部用完,直接抛出异常,这种情况一般是对方服务挂掉了,强依赖的场景下,自己的业务也是无法进行的。
    这两个方法很明显的问题就是,方法高度类似却又不同。
    正好最近在看 CompletableFuture 源码,源码中对于 Java8 中的函数式接口使用给予了我启发,这段重试的代码可以通过函数式编程的方式来进行优化。
    首先需要了解几个基础的函数式接口:

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

    函数式编程的特点就是将函数的实现放到调用者处,而不是函数内部,传递行为而不是传递值,提供了更高层次的抽象。
    大概理解这几个函数式接口的使用后,于是提炼出了如下工具类:

    1. /**
    2. * 重试工具类
    3. * 应用场景:接口调用重试
    4. *
    5. * @author WeJan
    6. * @since 2021-02-06
    7. */
    8. public class RetryUtil {
    9. private static final Logger LOGGER = LoggerFactory.getLogger(RetryUtil.class);
    10. /**
    11. * 方法重试调用(未执行成功才进行重试)
    12. * 不处理方法调用异常
    13. *
    14. * @param invokeMethod 执行方法
    15. * @param retryCondition 重试条件
    16. * @param fixedWaitMilliseconds 重试间隔时间(毫秒)
    17. * @param retryCount 重试次数
    18. * @param exception 达到最大重试次数,抛出指定异常
    19. * @return 方法返回
    20. */
    21. public static <U> U invoke(Supplier<U> invokeMethod,
    22. Predicate<U> retryCondition,
    23. long fixedWaitMilliseconds,
    24. int retryCount,
    25. Supplier<? extends RuntimeException> exception) {
    26. return RetryUtil.invoke(invokeMethod, retryCondition, fixedWaitMilliseconds, retryCount, null, exception);
    27. }
    28. /**
    29. * 方法重试调用(未执行成功才进行重试)
    30. * 可手动处理异常,设置默认值
    31. * @param invokeMethod 执行方法
    32. * @param retryCondition 重试条件
    33. * @param fixedWaitMilliseconds 重试间隔时间(毫秒)
    34. * @param retryCount 重试次数
    35. * @param exceptionally 执行方法发生异常处理,返回默认值, 为 null 则直接上抛异常
    36. * @param exception 达到最大重试次数,抛出指定异常
    37. * @return 方法返回
    38. */
    39. public static <U> U invoke(Supplier<U> invokeMethod,
    40. Predicate<U> retryCondition,
    41. long fixedWaitMilliseconds,
    42. int retryCount,
    43. Function<Exception, U> exceptionally,
    44. Supplier<? extends RuntimeException> exception) {
    45. if (invokeMethod == null || retryCondition == null || exception == null) {
    46. throw new NullPointerException();
    47. }
    48. U result = null;
    49. try {
    50. result = invokeMethod.get();
    51. } catch (Exception e) {
    52. LOGGER.error("", e);
    53. if (exceptionally != null) {
    54. return exceptionally.apply(e);
    55. } else {
    56. throw e;
    57. }
    58. }
    59. if (!retryCondition.test(result)) {
    60. return result;
    61. }
    62. if (retryCount > 0) {
    63. try {
    64. TimeUnit.MILLISECONDS.sleep(fixedWaitMilliseconds);
    65. } catch (InterruptedException ignored) {
    66. }
    67. LOGGER.error("retryCount: {}", retryCount);
    68. return invoke(invokeMethod, retryCondition, fixedWaitMilliseconds, --retryCount, exceptionally, exception);
    69. }
    70. throw exception.get();
    71. }
    72. }

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

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