客户端超时了就重试,这是网络世界的常态。怎么设计 HTTP 接口,让同一个请求重复提交不会重复执行?Idempotency-Key 怎么用、同时到达怎么办、key 被拿去干别的怎么办、key 能保留多久、有没有不用 key 的办法?这篇用真实的 HTTP 服务和 MySQL 逐个验证。
客户端:{'client 最终拿到订单号': 132, 'client 放弃了(没拿到响应)': 68};库里的订单数 336(逻辑请求 200 个,多出 136 个重复订单)
各种请求序列(每次尝试的结果)的个数:201:132;timeout→timeout→timeout:68
超时的那次其实已经(或者马上)在服务端建好了订单,客户端不知道,重试又建一个超时的请求其实已经在服务端处理,客户端不知道,重试又建一个,慢请求每次尝试都产生一个订单。
客户端:{'client 最终拿到订单号': 200, 'client 放弃了(没拿到响应)': 0};库里的订单数 200(多出 0 个);idem 表 200 行(DONE 200 行)
各种请求序列的个数:201:132;timeout→409→201(重放):68
慢请求:第一次超时(服务端还在处理)→ 第二次遇到 PROCESSING 返回 409 → 等 0.2 s 后第三次拿到保存的响应(带 Idempotent-Replayed: true);服务端从头到尾只执行了一次每个逻辑请求一个 key,重试沿用;服务端先占位,处理中返回 409,处理完保存响应,之后重放。
结果组合:201 + 409:100;库里订单数 100(应为 100) 两个同时到达的相同请求里只有一个抢到 PROCESSING 占位去执行,另一个拿到 409(正在处理中);订单只建一个
同一个 key 同时到达,靠唯一键只让一个请求执行,另一个得到 409。
第 1 次(amount=100):201 {'order_id': 637};第 2 次(同样内容):201 {'order_id': 637} Idempotent-Replayed=true;第 3 次(amount=999,同一个 key):422 {'error': '同一个 Idempotency-Key 对应了不同的请求内容'};库里订单数 1同一个 key 对应不同内容要拒绝(422),不能把旧响应还给新请求。
第 1 次:201 {'order_id': 638};1 秒后用同一个 key 重试:201 {'order_id': 639} Idempotent-Replayed=None;库里订单数 2(同一个请求被执行了两次)
key 的保留期必须比客户端可能重试的最长窗口(包括离线队列、人工重发)长key 记录要保留得比客户端最长的重试窗口久,否则过期后重试会再执行一次。
客户端:{'client 最终拿到订单号': 132, 'client 放弃了(没拿到响应)': 68};库里的订单数 200(多出 0 个);idem 表 0 行
各种请求序列的个数:200:132;timeout→timeout→timeout:68
PUT 本身就是幂等的:同一个外部订单号重复提交只会覆盖同一行,不需要额外的 key 表;但它要求客户端能提前生成稳定的订单号,而且“两次 PUT 之间别人改过”的情形要靠版本 / 条件请求(If-Match)另外处理客户端选稳定的资源 id,用 PUT 覆盖同一行,天然幂等,不需要 key 表。