接口重试机制引入,数据分析展示

Author Avatar
yujian95 4月 11, 2021
  • 在其它设备中阅读本文章

本文对系统请求外部系统异常情况时,增加重试机制API提升系统可用性,并减少异常数据。

导读

接手OA系统开发维护一段时间,发现主要的开发内容都是对审批结果回传外部系统,系统是通过触发监听事件,获取需要的参数后发送请求报文。

因此有如下俩个问题

  1. 监听事件代码基本一致,只是回传参数不一样而已。参数配置不对,容易出问题。
  2. 监听事件触发后,发送请求的过程容易出错。如外部系统不稳定或系统问题会导致,接收报文后,没有更新数据。

对于上述俩个问题,因为没有源码做出如下解决方案

  1. 编写代码生成器,根据需要回传的参数生成对应的监听事件代码、配置文件等。
  2. 为OA系统增加API请求重试机制,核心思路,记下请求报文写入数据库,编写重试代码重发请求。

第一点代码生成另写文章探讨,这里对第二点重试机制进行探讨。

核心概念讲解

什么是重试?

当接口请求异常,无法提供服务(例如服务发版,网络抖动等)导致请求失败的情况。对应这种情况,我们应当建立重试机制,以提高系统可用性。

重试,即当请求接口异常时,一种补充机制。主流的重试工具包,有spring-retryguava-retryeasy-retry等。

工具包对比

下列三个工具包都是线程安全;可以设置超时时间,重试次数,间隔时间,监听结果。

开发者 优点 缺点
spring-retry spring框架 集成spring框架; Spring retry 是基于AOP的,所以不要在同一个类中做方法内部调用,不然将使aspect增强失效,无法触发重试机制。
guava-retry 谷歌 功能灵活全面,入侵量小; 需要自己实现Callable接口。
easy-retry 阿里巴巴 使用方便仅添加注解即可,代码入侵量小; 需要另外创建数据表存储数据。

为什么要重试

场景:在远程调用超时、网络突然中断、外部系统发版或者接口产生问题时,发送的参数正确,由于外部系统问题,导致系统产生异常数据。

这时,我们需要在另外的时间,再次重试,以确保系统之间数据同步正常。因此需要增加重试机制,提高系统之间可用性,减少系统之间数据异常。

怎样实现重试

这里,我结合了业务需求对接口请求增加了数据分析统计,以及接口异常预警。

主要流程如下

  1. 对原有的接口方法统一,并优化实现。
  2. 存储需要重试的请求信息,异常情况。
  3. 根据特定条件重试请求,并更新请求重试结果。

根据需要绘制原型图

dashboard

统一请求方法,优化实现

核心类图

  • ApiPostService:请求方法接口
  • AbstractApiService:默认方法实现抽象类
  • TaApiServiceImpl、EpmsApiServiceImpl、CrmApiServiceImpl、ErpApiServiceImpl:具体方法实现类
  • ApiTool:反向代理类

12

请求方法接口

实现统一请求,这里根据业务需求分成俩种类型(流程中、流程结束)

public interface ApiPostService {

    /**
     * 流程中,请求
     *
     * @param param    请求参数
     * @param orderId  请求参数中的唯一值,一般取编号值
     * @param formCode 表单编码
     */
    void processPost(Map<String, Object> param, Object orderId, String formCode);

    /**
     * 流程完成,最后请求
     *
     * @param param    请求参数
     * @param orderId  请求参数中的唯一值,一般取编号值
     * @param formCode 表单编码
     */
    void finalPost(Map<String, Object> param, Object orderId, String formCode);
}

默认接口实现抽象类

public abstract class AbstractApiService implements ApiPostService {

    /**
     * 是否最终请求
     */
    public static final String YES = "Y";
    public static final String NO = "N";
    /**
     * 请求地址
     */
    public ApiUrlEnum urlEnum;

    public AbstractApiService(ApiUrlEnum urlEnum) {
        this.urlEnum = urlEnum;
    }

    /**
     * 校验请求结果,判断是否请求成功
     *
     * @param response 请求结果
     * @return 是否成功
     * @throws ApiFailException 请求失败异常
     */
    public abstract boolean isSuccess(String response) throws ApiFailException;

    /**
     * 请求是否成功
     *
     * @param param    请求参数
     * @param orderId  请求参数中的唯一值,一般取编号值
     * @param formCode 表单编码
     * @param isFinal  是否最终请求 是(Y),否(N)
     */
    public abstract void common(Map<String, Object> param, Object orderId, String formCode, String isFinal);

    /**
     * 流程中,请求
     *
     * @param param    请求参数
     * @param orderId  请求参数中的唯一值,一般取编号值
     * @param formCode 表单编码
     */
    @Override
    public void processPost(Map<String, Object> param, Object orderId, String formCode) {
        common(param, orderId, formCode, NO);
    }

    /**
     * 流程完成,最后请求
     *
     * @param param    请求参数
     * @param orderId  请求参数中的唯一值,一般取编号值
     * @param formCode 表单编码
     */
    @Override
    public void finalPost(Map<String, Object> param, Object orderId, String formCode) {
        common(param, orderId, formCode, YES);
    }
}

存储需要重试的请求信息

这里因为我主要是为OA系统对外部系统请求进行重试。结合业务需求设计的数据表。

请求日志表设计

Y:是、N:否、O:其他情况

create table ARROW_API_LOG
(
    ID                  VARCHAR2(64) not null,
    ORDER_ID            NVARCHAR2(128),
    SYSTEM_CODE         NVARCHAR2(32),
    FORM_NAME           NVARCHAR2(32),
    URL                 NVARCHAR2(512),
    REQUEST_PARAM       CLOB,
    RESULT              CLOB,
    IS_SUCCESS          NVARCHAR2(32) default 'N',
    IS_FINAL            NVARCHAR2(32) default 'N',
    IS_RETRY_SUCCESS    NVARCHAR2(32) default 'O',
    DEAL_DATE           DATE,
    ERROR_MESSAGE       NVARCHAR2(1024),
    RETRY_DATE          DATE,
    RETRY_ERROR_MESSAGE NVARCHAR2(1024),
    primary key ()
)
/

对应系统请求方法实现类

以CRM系统为例:

public class CrmApiServiceImpl extends AbstractApiService {

    public CrmApiServiceImpl() {
        super(ApiUrlEnum.CRM);
    }

    public CrmApiServiceImpl(ApiUrlEnum urlEnum) {
        super(urlEnum);
    }

    /**
     * 校验请求结果,判断是否请求成功
     *
     * @param response 请求结果
     * @return 是否成功
     * @throws ApiFailException 请求失败异常
     */
    @Override
    public boolean isSuccess(String response) throws ApiFailException {
        if (StrUtil.isEmpty(response)) {
            throw new ApiFailException("response is null.");
        }

        try {
            JSONObject result = JSONObject.fromObject(response);
            if ("true".equalsIgnoreCase(result.getString("success"))) {
                return true;
            } else if (result.containsKey("auditFlag") && "true".equalsIgnoreCase(result.getString("auditFlag"))) {
                // 已审核通过,重复单据
                return true;
            } else {
                throw new ApiFailException(result.getString("message"));
            }
        } catch (Exception e) {
            throw new ApiFailException(e.getMessage(), e);
        }
    }

    /**
     * 请求是否成功
     *
     * @param param    请求参数
     * @param orderId  请求参数中的唯一值,一般取编号值
     * @param formCode 表单编码
     * @param isFinal  是否最终请求 是(Y),否(N)
     */
    @Override
    public void common(Map<String, Object> param, Object orderId, String formCode, String isFinal) {
        JSONObject json = null;
        String response = null;
        try {
            json = JSONObject.fromObject(param);
            response = HttpUtil.postJson(json, urlEnum.getUrl());
            if (isSuccess(response)) {
                ApiLogUtils.success(orderId, urlEnum.getCode(), formCode, urlEnum.getUrl(), json.toString(), response, isFinal);
            }
        } catch (Exception e) {
            // 记录异常记录
            ApiLogUtils.fail(orderId, urlEnum.getCode(), formCode, urlEnum.getUrl(),
                    json != null ? json.toString() : "", response, isFinal, e.getMessage());
        }
    }
}

重试请求,并更新请求结果

核心类图如下:

  • ApiService:请求方法接口
  • AbstractApiService:默认方法实现抽象类
  • ApiTool:请求方法代理类
  • ApiRetryerBuilder:重试构造器
  • RetryLogListener:重试监听器
  • SpinBlockStrategy:自定义重试策略

AbstractApiService

我这里主要用了guava-retry实现接口请求重试。

定时重试失败请求记录

List<ApiLog> needRetryList = apiLogService.listNeedRetry(null,
        DateUtil.beginOfDay(startDate), DateUtil.endOfDay(endDate));
Retryer<Boolean> retryer = apiRetryerBuilder.build();

Callable<Boolean> apiCall = () -> {
    int failCount = 0;
    for (ApiLog apiLog : needRetryList) {
        if (!apiLogService.retry(apiLog)) {
            log.error("retry fail, id:[{}], system:[{}], formCode:[{}], isFinal:[{}].", apiLog.getId(),
                    apiLog.getSystemCode(), apiLog.getFormName(), apiLog.getIsFinal());
            // 统计失败记录数
            failCount++;
        }
    }
    log.info("retry api failCount: [{}]", failCount);
    //  存在失败情况时,重试
    if (failCount == 0) {
        return true;
    } else {
        throw new ApiFailException("retry fail count: " + failCount);
    }
};
try {
    retryer.call(apiCall);
} catch (Exception e) {
    log.error(e.getMessage());
    return new ReturnT<>(500, e.getMessage());
}
log.info("end api retry");
return ReturnT.SUCCESS;

重试功能上线后效果

该功能上线后,有效减少了系统请求异常情况。

这里结合图表,显示请求数据内容,这里展示测试情况。

  • 数据量概览(分析业务数据量)
  • 重试效果展示(统计重试成功率)
  • 接口可靠性分析(对出错概览较高的接口进行优化,并预警)

image-20210406231504301

本站永久域名「 yujian95.cn 」,也可关注微信公众号「 编程图解 」找到我。
本文链接:http://yujian95.cn/post/explore/how2retry.html