Skip to content

API 幂等性、超时重试与移动端数据最终一致性 ​

回到总览:后台 / API / 数据层
相关模块:RESTful API 设计规范与移动端契约约定 · Token 鉴权体系、JWT、OAuth2 与移动端双 Token 自动无感刷新

一句话定义 ​

API 幂等性(Idempotency)是指客户端使用相同的参数对同一个 API 发起一次或多次请求,所产生的系统服务端状态变更与影响均完全相同的属性;结合客户端指数退避重试与幂等号 (RequestId / Nonce) 是解决移动弱网环境下重复提交与数据一致性的核心基石。

代码索引 ​

计划补齐的实验:

  • labs/shared/backend-data/api-idempotency-demo/ — RequestId 幂等号拦截、指数退避重试与本地事务一致性测试

为什么需要 ​

  • 为什么在移动端发起“提交订单”或“扣款支付”请求时,即使网络超时抛出 TimeoutException,也不能盲目在 Catch 块中无脑重试?
    • 一句话答:网络超时可能发生在服务端处理成功后返回响应的链路中;此时服务端订单已成功创建,若客户端在没有带上幂等号的情况下盲目重试,会导致服务端重复创建订单或重复扣款。
  • 为什么 10 年 Android 工程师做移动端架构时必须建立“客户端幂等防重”认知?
    • 一句话答:移动网络(5G/4G/Wi-Fi 切换)具有天然的不稳定性;通过在 Header 注入唯一的 X-Request-Id,配合服务端的 Redis 防重锁,才能在保障弱网自动重试体验的同时防止数据污染。

底层机制 ​

1. HTTP 方法规范中的天然幂等性 ​

HTTP 方法是否幂等是否安全 (Safe)语义说明
GET是是仅查询数据,不改变服务端状态
PUT是否完整替换覆盖对象,重复多次结果相同
DELETE是否删除特定资源,重复删除返回 404 或已删除
POST非幂等否创建新资源,重复请求会产生多个新资源

2. 幂等号 (RequestId / Nonce) 防重流程 ​

text
[移动客户端] ─── (1. 生成 UUID: req_10086) ───► [后端网关 / API]
     │                                               │
     │                                               ▼
     │                                    [Redis 防重锁/查重]
     │                                       - 若 Token 存在: 返回历史缓存结果
     │                                       - 若 Token 不存: 执行业务并缓存 Token
     │                                               │
     ◄─── (2. 200 OK / 扣款成功) ─────────────────────┘
  1. 唯一标识生成:客户端在进入“提交”页面或点击按钮时,生成全局唯一的 RequestId(如 UUID)。
  2. 请求携带:在 HTTP Header 中注入 X-Request-Id: req_10086。
  3. 服务端校验:服务端利用 Redis 的 SETNX 尝试加锁。若已被处理过,直接返回上一次处理成功的 Result,不再重复执行扣款逻辑。

3. 客户端指数退避重试 (Exponential Backoff with Jitter) ​

在重试网络请求时,避免固定时间间隔重试引发服务端流量雪崩: 重试等待时间 = BaseInterval * 2^retryCount + RandomJitter

Android / Flutter / Web / Backend 对照 ​

维度移动客户端 (Android / Flutter)后端 Gateway / Service
幂等实现按钮防抖 (Debounce) + 注入 X-Request-IdRedis SETNX 防重 + 数据库唯一索引
重试策略OkHttp Interceptor 指数退避加随机抖动gRPC Retry Policy / Resilience4j
一致性本地 Room 数据库与服务端异步最终一致分布式事务 (Saga / TCC / 2PC)

常见场景与代码实现 ​

1. OkHttp 指数退避重试拦截器 ​

java
public class ExponentialBackoffInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response = null;
        boolean responseOK = false;
        int tryCount = 0;
        int maxRetries = 3;

        while (!responseOK && tryCount < maxRetries) {
            try {
                response = chain.proceed(request);
                responseOK = response.isSuccessful();
            } catch (Exception e) {
                tryCount++;
                if (tryCount >= maxRetries) throw e;
                // 指数退避抖动计算: 1s, 2s, 4s...
                long backoff = (long) (Math.pow(2, tryCount) * 1000 + Math.random() * 200);
                try { Thread.sleep(backoff); } catch (InterruptedException ignored) {}
            }
        }
        return response;
    }
}

常见误配、事故后果与排障 ​

1. 事故:网络超时无脑重试导致账户重复扣款 ​

  • 误配原因:在 POST /api/v1/order/pay 接口中,客户端捕获 SocketTimeoutException 后盲目重新发起相同的 POST 请求,且未带幂等号。
  • 后果:实际上第一次请求服务端已成功完成扣款,仅响应回包超时。重试请求导致服务端再次扣款,引发严重线上资金事故。
  • 排障与修法:所有涉及写数据的非幂等接口,必须增加全局唯一 RequestId;网络超时重试前先调用“订单状态查询”接口确认为未支付再发起重试。

与相近概念对比 ​

概念核心关注点解决方案
API 幂等性防止重复请求导致数据重复修改请求幂等号 X-Request-Id + 数据库唯一索引
强一致性 (Strong)读操作必定能读到最新的写结果2PC / Raft 协议(性能较低)
最终一致性 (Eventual)允许短时间数据不一致,最终达成一致消息队列 (MQ) + 本地事务表 + 轮询补偿

对应实验 ​

计划补齐的实验:

  • labs/shared/backend-data/api-idempotency-demo/ — RequestId 幂等号拦截、指数退避重试与本地事务一致性测试

复习检查题 ​

  1. 为什么 HTTP POST 请求默认是非幂等的,而 PUT 和 DELETE 是幂等的?

    答:因为 POST 的语义是向服务端创建新资源,多次发起 POST 会产生多个不同的新资源(如创建多个新订单);而 PUT 的语义是用传入的数据完整替换覆盖指定 URI 的资源,DELETE 的语义是删除指定 URI 的资源,无论执行一次还是十次,最终服务端的资源状态都是一样的。

  2. 在移动端弱网环境下,为什么重试等待时间推荐使用“带随机抖动的指数退避(Exponential Backoff with Jitter)”?

    答:如果采用固定时间间隔重试(如每 1 秒重试一次),当基站发生短暂停机恢复时,大量客户端会在同一时间点同时发起重试,造成服务端遭遇严重的“重试雷暴(Retry Storm)”冲垮数据库。指数退避(1s, 2s, 4s...)分散了重试时间,加上随机抖动(Jitter)可以彻底错开不同客户端的请求时间点,保护服务端安定。

速记 ​

  • 幂等防重:非幂等写接口必带 X-Request-Id,服务端 Redis 锁防重。
  • 退避重试:网络超时重试加指数退避与随机抖动,切忌固定间隔抢铺。
  • 一致性原则:移动端追求最终一致性,本地快照 + 后台异步补偿。

站点构建时间:2026/8/24 23:43:17