AIGC标识 实名认证接口 Java 对接的进阶实践:超时、重试与对账的工程化封装

上个月写过一篇 Java 版身份证核验接口的基础接入教程,讲了签名工具类和 Demo 的改进。这篇是进阶篇——基础接入跑通之后,生产环境真正会让你掉头发的是三件事:超时怎么设、重试怎么控、账单怎么对。

这三件事没有一篇接入文档会写在第一页,但生产事故和月底的账单纠纷几乎都出在这里。以下代码都可以直接放进你的接入层。

一、超时:按链路的最慢环节配置

很多团队按"我们平时 API 都设 5 秒"的习惯去配,然后发现四要素核验大量超时——因为银行卡核验走银联/银行通道,30 秒内的时延波动是常态。

public class VerifyHttpClient {
    private final CloseableHttpClient http;

    public VerifyHttpClient() {
        RequestConfig config = RequestConfig.custom()
                .setConnectTimeout(5_000)      // 建连接 5s
                .setConnectionRequestTimeout(3_000) // 从连接池取连接 3s
                .setSocketTimeout(35_000)      // 读超时 35s:按最慢链路给足
                .build();
        this.http = HttpClients.custom()
                .setDefaultRequestConfig(config)
                .setMaxConnTotal(50)           // 总连接数按服务商并发配额定
                .setMaxConnPerRoute(30)
                .build();
    }
}

关键认知:超时 ≠ 失败,超时是"结果未知"。 35 秒放弃的请求,服务端很可能已经完成核验并计费——这个认知是对账不出鬼账的前提。

二、重试:白名单制 + 预算制

第一版重试逻辑通常长这样:catch (Exception e) { retry(); }——这是账单炸弹。正确的做法分两步。

第一步:错误码分级,只重试"系统异常"

public enum VerifyResult {
    SUCCESS(0, "核验通过", Category.BUSINESS_OK),
    MISMATCH(1108, "信息不一致", Category.BUSINESS_REJECT),  // 业务拒绝:重试无意义
    UPSTREAM_TIMEOUT(2001, "上游超时", Category.RETRYABLE),
    GATEWAY_ERROR(2002, "网关异常", Category.RETRYABLE);

    enum Category { BUSINESS_OK, BUSINESS_REJECT, RETRYABLE }

    public boolean retryable() {
        return category == Category.RETRYABLE;
    }
}

业务拒绝码永远不进重试队列——重试一万次"信息不一致"还是不一致,每次都是真金白银。

第二步:重试预算,给总量上闸

public class RetryBudget {
    private final int budget;
    private final AtomicInteger used = new AtomicInteger();

    public RetryBudget(int totalRequests, double ratio) {
        this.budget = (int) (totalRequests * ratio); // 重试总量 ≤ 原始量 20%
    }

    public boolean allow() {
        return used.incrementAndGet() <= budget;
    }
}

批量任务里:预算耗尽,剩余失败进"待补跑"队列而不是继续重试。这条闸门保证无论上游多糟,你对它的加压有确定上限。

第三步(经常被漏):退避加抖动

private long backoffMs(int attempt) {
    long exp = (long) Math.min(30_000, 1000L * (1L << attempt));
    return (long) (Math.random() * exp); // 全抖动:同一批超时的请求错峰回来
}

不加抖动的指数退避,会让同一批超时的请求几乎同一时刻重试,形成锯齿状洪峰——实测洪峰差 5-8 倍。

三、对账:流水四态 + 幂等键

月底账单对不上,九成是本地流水就没记对。

流水必须区分四种状态,混在一起的账没法对:

public enum LocalStatus {
    SUCCESS,       // 明确成功
    TIMEOUT,       // 结果未知(超时,可能已计费!)
    BIZ_REJECT,    // 业务拒绝(通常计费:核验确实发生了)
    SYS_ERROR      // 系统异常(按计费清单确认)
}

幂等键从第一天就要有:

public String idempotencyKey(String bizNo, int attempt) {
    return bizNo + ":v1"; // 关键:重试时保持同一个键,服务端据此去重
}

注意 attempt 参数故意不参与拼键——重试的必须是同一个幂等键(服务端才能识别为同一笔业务),新业务才换新键。

四、一张生产检查清单

□ 读超时按最慢链路(银行通道 30s+),不按习惯 5s
□ 错误码三分类:业务通过/业务拒绝/可重试,白名单重试
□ 重试预算 ≤ 原始量 20%,耗尽即停
□ 退避必加抖动
□ 幂等键全覆盖可重试请求,重试保持同键
□ 流水四态落库,超时状态单独标记
□ 月度与服务商对账:计费数 vs 本地(成功+超时实成功)
□ 争议处理:流水导出 + 幂等键证明 + 书面计费清单

相关链接

下一篇计划写《超时后的状态查询设计:先查后重》,有兴趣的可以关注。

posted @ 2026-10-10 12:09  awayaz  阅读(4)  评论(0)    收藏  举报