每个坐落在不可靠的网络后面的API最终都面临着同样的问题:一个客户端发送请求,在响应到达之前连接下降,客户端不知道操作是否发生.
付了钱没?
订单被创造两次了吗?
客户端的唯一安全动作是重试——也就是说,当同一个"创造出这个东西"的请求不止一次地到达时,你的服务器需要一个故事来描述会发生什么.
这个故事是独一无二的钥匙, 并且把细节弄正确 比它最初看起来更微妙。
核心思想 客户端每次逻辑操作一次生成一个独特的符号——典型的UUID,并将它附在操作的每次重试上: 服务器的职责是保证无论用这把钥匙的请求到达多少次,副作用(充值卡,创建订单,发送电子邮件)最多一次发生,每次重试都会得到原请求产生的相同响应.
请注意,这不是:它不会被请求机构破坏。
有两件相同机构但无密钥的请求,是两个部件的两个不同订单。
关键是它们被标记为"同样的尝试",而不是有效载荷.
那种天真的方法, 以及它为什么打破了 一个共同的第一通道是一张桌子,比如: 在每次请求中:检查密钥是否存在,如果存在,则返回已缓存的响应;否则要工作并插入结果.
这看起来是正确和错的,具体来说是:它有一个种族条件.
两次复试可以同时到达(一个客户端超时,并在第一次仍在飞行中时发射第二次尝试),两者都错过了缓存检查,并同时执行基础操作.
你给卡开过两次了 做检查和做原子 固定是使用数据库本身的货币控制而不是应用程序级的检查,在进行工作前声称有钥匙: 如果返回一行,你赢得了这个键——继续操作,然后用真实的响应更新一行并翻转到.
如果它不归还, 别人已经要求了这个钥匙。
现在,你有三个小写要明确处理: 状态 = “ 已完成 ” —— 返回存储的响应 。
这是重试成功的路径, 也是人们唯一设计的。 status = “ in progress” – 使用同一密钥的另一项请求现在仍在执行中,很可能是真正同时进行的重试(客户端超时,在第一次尝试返回之前会发出重复的信号) 。
这里的正确反应通常带有"很快重复"的提示,而不是默默地屏蔽,因为只要最初的请求需要并且可以在负载下级联,就把连接阻断.
状态 = “ 失败” —— 初始尝试在完成前出错 。
这是否安全可以重试,取决于失败发生在承诺副作用之前还是之后,这正是下一节重要的原因.
命令副效果和密钥更新 危险的窗口介于"副作用发生"和"关键记录说发生了"之间.
如果您的支付提供商向卡收取费用,然后在写入前程序崩溃,则密钥会永远被卡住(或者,如果你有一个崩溃处理器),合法的重试将被拒绝,或者——更糟糕的是,如果你设计了失败状态允许重试——将再次向卡充电.
两种实际的出路: 同一交易,在可能的情况下。
如果副作用本身是数据库写入(创建命令行),则在与密钥更新相同的交易中执行.
要么承诺要么不承诺 要么根本就没有窗户 外侧效应,下游同能.
如果副作用是给第三方(付款处理器)打电话,那么,通过这个也叫“一能”键——大多数付款API(Stripe, Braintree, Razorpay)都从本质上支持这一点。
然后,一个崩溃的行的恢复路径是: 用同样的下游键重新发出下游呼叫.
如果已经发生,处理器会返回原始结果而不是双充电.
你自己的键盘BEC