LabHub
学习 学习路径 课程

멱등성 — 두 번 눌러도 한 번만 결제되게 · 재시도와 중복 · 讲解

재시도는 피할 수 없다

在 LabHub 中继续学习

一句话总结

无法避免再次尝试。所以重复应该由服务器阻止。

概念图: 重复应该由服务器阻止。 · 到达服务器 · 没有收到回复 · 客户没有办法区分这个。

为什么需要—客户无法区分

我发送了结算请求,但没有收到回复。两者中的哪个?

1. 请求没有到达服务器→需要重新发送
2. 到达后已经处理了,但是没有收到回复→再发送的话会被计费两次。

客户没有办法区分这个。所以不管选择哪一个都是错误的。

所以客户只能重新尝试,让服务器识别第二次

横向性高度

客户端每次请求都会创建并粘贴唯一的键

POST /payIdempotency-Key: 9f2c-...-a1{"user":"u1","amount":1000}

重新尝试时使用相同的密钥。如果服务器见过该密钥,则不会处理,而是将保存的响应原封不动地返回。

核心是这个——第二次请求也收到成功响应。不是错误。从客户的角度来看,“请求一次就成功一次”,这是正确的。

存放在哪里

不能用记忆词典。理由有两个。

1. 重新开始的话就会忘记
2. 服务器有很多台——1号帕德记得的,2号帕德不知道。

2号更常见。用一个pad测试的话,会完美运行,但一旦附加自动缩放,就会出现重复。

所以存储必须是大家一起看的地方——DB或Redis。

如果同一高度有不同的正文的话

Idempotency-Key: k1   {"amount": 1000}Idempotency-Key: k1   {"amount": 99999}

如果安静地通过第二个,将1000韩元的回复退还给99999韩元的请求。客户不知道99999韩元已经支付了。

因此,请求正文的哈希码一起保存,如果不同,则拒绝422。这是防止重复使用密钥的漏洞被悄悄掩盖的装置。

同时身高相同的话

这是最常出错的位置。

row = db.get(key)          # 없다if not row:                # ← 열 개가 동시에 여기를 통과한다    charge()    db.put(key, response)

在查询和插入之间,别人插手了。单纯的方式在负担加重的瞬间就会崩溃。

要做好,就要对关键字施加唯一限制,让插入失败的一方等待后读取存储的响应,或者将其锁定。使用数据库已经拥有的保障是最便宜的。

保存的回复要保留到什么时候?

不能永远在一起。一般24小时左右后删除。

太短的话,重复的延迟重试会造成重复,太长的话,缓存会一直增加。请将其设置为比客户端重试窗口宽裕。

遇到什么错误后再试一次

| 回复 | 重新尝试 | 为什么|
|---|---|---|
| 没有回复(超时)|✅|不知道是否到达了|
| 500502503 | ✅ | 服务器方面暂时问题|
| 429 | ✅(等了一会儿)|Retry-After遵守|
| 400422 | ❌ | 请求错误。永远一样|
| 404 | ❌ | 一般是永久性的|

然后不断增加间隔(exponential backoff),并给每个客户端摇晃(jitter)。否则,试图恢复的服务器会在同一瞬间收到大量重试,再次崩溃。

附在什么请求上

附在金钱移动或制作某物请求上。POST,还有副作用的PATCH.

GET·PUT·DELETE本来是设计成排序的方法。PUT即使用同样的东西重复使用两次,结果也一样,DELETE第二个是“已经没有了”。就是设计是这样的,实现不会自动变成那样

回复保存的回复时的陷阱

贴上横幅后,出现了新的问题。**把最初的回答原封不动地回复的
事情**并不总是正确的。

状态可能在这期间发生了变化。 创建订单后用户取消了,重新尝试
如果把来时的保存的201响应(“生成”)原封不动地还给客户,客户就会有有效的订单。
相信有。保存应该是请求的结果,客户端现在
如果需要了解状态的话,会在回复中一起提供查询路径。

如果把全部回复都保存的话,会变大。如果把整个正文都保存的话,存储器会很快用完。
需要的通常只有状态代码和创建资源的标识符,所以只包含那个。
剩下的会重新制作的。

设定保存期限后删除。永远不需要额外的键盘。客户的
比重新尝试窗口(一般24小时)要宽裕地保留,然后擦掉。如果不擦掉,表格就会
不断增长,索引越大,原本快速的查询就会变慢。

delete from idempotency_keys where created_at < now() - interval '7 days';

决定如果过期的密钥再次出现该怎么办。 悄悄地重新处理的话会造成重复,
拒绝的话,非常晚的再尝试会失败。拒绝的一方是安全的,到时候
明确通知“此密钥已过期,请用新密钥重新发送”。

用回复头衔告知是否是重新尝试。Idempotency-Replayed: true相同的头衔
粘贴的话,客户和调查的人都会保存这个回答是否是新处理的。
我知道是不是。如果留到日志里的话,“请求增加了两倍”是重新尝试还是实际增加
马上就分开。

不信任密钥本身。因为是客户端创建的值,所以和其他用户的密钥和
可以重叠。必须与组织或用户识别符一起掌握唯一性才能获得别人的回复
没有接受过的事情。

在现场

重复结算的报告通常不是由用户发现的,而是由结算系统首先发现的。用户只记得“好像没支付,所以又按了一次”,而中间发生的事情只留在了服务器日志中。

所以,在发生错误后,连接到主机非常困难。因为已经离开的客户端不会发送密钥,所以服务器需要暂时同时接受有密钥和没有密钥的请求。如果不先确定如何处理没有密钥的请求——是否直接通过、拒绝、设定期限暂缓——那么在转移过程中会造成更大的混乱。

如果存在像移动应用程序一样无法强制部署的客户端,也会写下服务器根据请求正文的哈希值临时生成密钥,只在短时间内阻止重复的方法。虽然不是完全的,但会过滤掉用户连续两次点击的最常见情况。