Skip to content

接口重试与幂等性设计

超时控制递减原则、指数退避加抖动防重试风暴、接口幂等性 5 大落地套路(防重 Token、唯一索引、状态机、乐观锁、分布式锁)。

Updated View as Markdown
For humans

接口重试与幂等性设计

校招考察频度:★ 高频(核心必背·高频生死线) 一句话速记:网络不可靠必须靠重试兜底,而重试必然带来重复;防重试风暴靠指数退避加随机抖动,抗重复请求靠业务幂等性(防重 Token、数据库唯一索引、状态机条件更新、乐观锁版本号)。

在分布式微服务架构中,“网络永远是不可靠的”。一次 RPC 或 HTTP 请求超时,并不代表服务端没有执行(可能请求到了且执行成功,只是响应报文在回传途中丢失)。此时若客户端发起重试,就会导致用户被重复扣款或生成两个重复订单。重试与幂等是一体两面的孪生兄弟。


一、0 基础生动类比:去餐厅点餐模型

概念现实点餐场景分布式系统对应逻辑
超时(Timeout)顾客叫服务员点单,等了 1 分钟服务员没理会。客户端发起请求后,不能无限期挂起线程,必须设置最长等待时限。
重试(Retry)顾客再次呼叫服务员:“服务员,刚才那道菜下了吗?”请求失败或网络超时时,自动化再次发起调用进行补偿。
重试风暴(Retry Storm)厨房着火上菜慢,全餐厅 200 个顾客每隔 1 秒疯狂催菜一次,后厨直接彻底崩溃!下游服务响应慢时,成千上万个客户端同时无脑高频重试,把原本微弱恢复的下游彻底打死。
幂等(Idempotence)不管顾客催单多少次,服务员拿看菜单上的唯一桌号 + 订单编号:“您放心,这道菜只会炒一份,不会重复上菜”。同一个操作无论执行 1 次还是 100 次,系统产生的最终业务状态与副作用完全相同。

二、超时控制:调用链路的“层层递减原则”

在多层微服务调用链中(客户端 ➔ 网关 ➔ 服务 A ➔ 服务 B ➔ 数据库):

[客户端超时: 5s] ──> [网关超时: 4s] ──> [服务 A 超时: 3s] ──> [服务 B 超时: 2s] ──> [DB 超时: 1s]
  • 反面教材(倒挂雪崩):若服务 A 超时设为 1 秒,而下游服务 B 设为 3 秒。当 B 执行了 1.5 秒时,A 早就认定超时并掐断连接;而 B 依然毫无知觉地继续拼命消耗 CPU 读写数据库。这会导致上游已经放弃并报错,下游还在无意义空耗资源。
  • 铁律:调用链路上的超时时间必须从上游到下游“层层递减”。

三、重试策略:指数退避与随机抖动(Full Jitter)

指数退避 + 随机抖动防惊群重试

1. 为什么不能“立即重试”或“固定间隔重试”?

下游一旦发生毛刺宕机,不可能在几毫秒内立刻恢复。如果几万个客户端以固定的 1 秒间隔同时发起重试,所有重试请求会在同一毫秒整齐划一地轰炸下游,形成**“惊群效应”与重试风暴(Retry Storm)**。

2. 指数退避加抖动公式(Full Jitter)

delay = random(0, min(max_backoff, base * (2 ^ attempt)))
  • 间隔时间成倍拉长(1s -> 2s -> 4s -> 8s),给下游留出宝贵的重启与 GC 恢复时间。
  • 引入随机抖动(Jitter),将原本重合的重试流量在时间轴上彻底打散抹平。

四、接口幂等性 5 大工业级落地套路

1. 防重 Token 机制(针对前端表单重复点击)

  • 时序流程:
    1. 用户进入提交页面,前端先调用后端接口获取一个全局唯一的临时令牌 token(存储在 Redis 中,如 SET token_123 1 EX 300)。
    2. 用户点击“立即付款”,请求携带该 token。
    3. 后端执行 Redis 原子删除:DEL token_123(或 Lua 脚本先查后删)。
    4. 若删除成功(返回值 1),说明是首次请求,放行执行业务;若删除失败(返回值 0),说明该 Token 已被消费过,直接拒绝!

2. 数据库唯一主键 / 唯一索引(最坚硬的底线兜底)

  • 依据业务属性提取业务唯一约束键(例如:订单表的 order_no,支付流水表的 pay_serial_no,防重表的 req_id)。
  • 多次重复提交时,底层数据库直接抛出唯一键冲突异常(MySQL 1062 Duplicate entry,Java 抛出 DuplicateKeyException),事务天然回滚,数据坚如磐石。

3. 业务状态机天然幂等

  • 针对订单、工单等生命周期严格的状态流转,在更新 SQL 中带上前置状态约束:
    -- 只有当前处于 UNPAID 状态时才允许变更为 PAID
    UPDATE orders SET status = 'PAID', updated_at = NOW() 
    WHERE order_id = 'ORD_999' AND status = 'UNPAID';
  • 若重复请求到达,由于状态已经变成了 PAID,该 SQL 执行的 affected_rows 影响行数为 0,直接判定为重复操作并返回成功即可,天然幂等。

4. 版本号机制(乐观锁)

  • 在数据表中增加 version 字段:
    UPDATE account SET balance = balance - 100, version = version + 1 
    WHERE user_id = 888 AND version = 5;
  • 只要版本号不匹配,更新直接失败,防止并发写冲突。

5. 分布式锁(配合状态存储)

  • 针对高并发瞬时请求,先尝试以业务 ID 加 Redis 分布式锁(SET order_id NX EX 30)。
  • 抢到锁的线程才能执行业务,未抢到锁的线程自旋等待或直接返回“正在处理中”。

五、知识脉络关联网(Knowledge Highway)


六、校招高频面试追问

  1. HTTP 协议中哪些方法是天然幂等的?哪些不是?
    • 天然幂等:
      • GET:纯只读查询,查 100 次结果一致且对服务端无副作用。
      • PUT:全量替换资源(如把年龄改成 18 岁,执行 10 次年龄依然是 18 岁)。
      • DELETE:删除特定 ID 资源(第 1 次删除返回成功,第 2 次返回 404 或已删除,系统数据状态相同)。
    • 非幂等:
      • POST:创建新资源或追加数据(发 10 次会创建 10 条重复记录)。
      • PATCH:局部修改资源(如相对累加 balance = balance + 10)。
  2. 幂等接口如果判定为重复请求,应该给客户端返回成功还是报错?
    • 必须返回成功(或原首次调用的业务结果)!
    • 切忌直接给客户端抛 500 或业务错误提示:“订单已存在”。因为从客户端视角看,发起重试往往是因为网络抖动没拿到上次的成功响应。如果给它报错,前端会以为支付失败,诱导用户换方式重复操作;正确做法是服务端在幂等表中查到旧结果,原样包装成功并返回。
Navigation

Type to search…

↑↓ navigate↵ selectEsc close