bigywater.tech
Flask · 107

Idempotency-Key

网络超时重试会不会重复下单?Idempotency-Key 怎么实现?并发的重复请求怎么挡?这篇用真实的超时重试与 6 个并发请求,把朴素实现和原子实现的差别量出来。

2 个订单
没有幂等键,超时重试后库里的订单数(用户只下了一单)
6 → 1
6 个并发的相同请求:朴素实现与先占位实现写入的订单数
409 / 422
请求处理中与同 key 不同请求体时的状态码

超时重试

实测 · 本机执行输出
  第一次请求:客户端超时 → TimeoutError
  重试: 201 {"id":2}
  结果:库里有 2 个订单(用户只下了一单)

上一篇:RESTful URL 设计;下一篇:API Token 认证。

顺序重试

实测 · 本机执行输出
  第一次: 201 {"id":1} | 耗时 389 ms
  第二次: 201 {"id":1} | Idempotent-Replayed = true | 耗时 4 ms
  库里订单数: 1
  同一个 key 换了请求体 → 422 {"error":"Idempotency-Key reused with a different request body"}
  不带 key → 400 {"error":"Idempotency-Key required"}

并发重复请求

实测 · 本机执行输出
  朴素:先查,没有再写               状态码分布 {201: 6} | 库里订单数 6
  原子:先 INSERT key 占位       状态码分布 {409: 5, 201: 1} | 库里订单数 1

6 个线程同时发出相同 key 的请求,服务端处理时间 0.5 秒来放大竞争窗口。

分步演示

Idempotency-Key
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动。Flask-SQLAlchemy 3.1.1、psycopg 3.3.6、自己起的 PostgreSQL(非默认端口)、gunicorn gthread
    • 没有验证:key 的过期清理(TTL)、处理中途进程崩溃留下 processing 记录的恢复、把 key 存在 redis 里的实现、保存的响应里的头与状态码的完整重放、IETF Idempotency-Key 草案的全部细节
    京ICP备2026067502号-1京公网安备11010502063487号