每个会修改数据的请求都必须使用,也是超时之后依然有效的唯一机制。
每个会修改数据的请求都必须携带一个幂等键:
Idempotency-Key: 5f1c3d9e-0b47-4c2a-9d3e-6a1b8c2f4e77
每个逻辑操作使用一个 UUID。不是每次重试一个——关键就在于重试时发送的是同一个键。
你的请求超时了。连接在你的进程和我们的进程之间的某处断开了。你不知道付款是否已经创建。
用同一个键重试,你会得到同样的答复:第一次尝试时存储的响应,逐字节相同,状态码也相同。不会产生第二笔付款。
没有这个机制,“刚才那笔成功了吗”这个问题就没有安全的答案,因为唯一的查证方法——再创建一次——恰恰是你想要避免的事。
409 idempotency_key_reused。键代表的是一个操作;请求体不同,就是另一个操作冒用了它的名字。409 idempotency_request_in_flight。稍后再重试。常见的错误是在重试循环内部生成键:
// 错误写法——每次尝试都是一个新操作
for (let i = 0; i < 3; i++) {
await post('/v1/payments', body, { 'Idempotency-Key': crypto.randomUUID() });
}
// 正确写法——整个操作只用一个键,每次尝试都复用它
const key = crypto.randomUUID();
for (let i = 0; i < 3; i++) {
try { return await post('/v1/payments', body, { 'Idempotency-Key': key }); }
catch (e) { if (!retryable(e)) throw e; }
}
如果你有自己的订单号,就用它——order_1837 比新生成的 UUID 更适合做键,因为进程重启后它依然不变。
速率限制器被刻意安排在幂等层之前运行。如果在键被占用之后才写入 429,它就会成为这个键永久存储的答复,毁掉你用来查明超时的付款是否成功的唯一机制。所以 429 永远不会被存储,用同一个键重试依然有效。