计算机科学

什么是幂等性?为什么支付按钮点两次也安全

幂等性是指多次发送相同请求时,结果仍然相同的性质。本文总结网络故障和重试时需要幂等性的原因、GET、PUT、DELETE与POST的区别,以及如何实现Idempotency-Key。

4 分钟阅读
什么是幂等性?为什么支付按钮点两次也安全 封面图

点击支付按钮后,页面却卡了很久,难免让人不安。你会想再点一次。

可是点两次,不会支付两次吗?

设计良好的系统不会。能保证这一点的性质有一个名字。

这就是幂等性。

这个概念会出现在后端文档、HTTP规范和支付API指南中,但定义本身一行就能说清。真正困难的是“为什么它如此重要”以及“如何实现”。

本文将回答这两个问题。

先看核心总结。

  1. 幂等性:相同请求发送一次或多次,结果都相同的性质
  2. 重要原因:网络会失败,失败后需要重试,而要让重试安全,操作就必须具备幂等性
  3. 在HTTP中,GET、PUT、DELETE具有幂等性,POST不具有幂等性
  4. 让POST安全的实用机制是Idempotency-Key

定义:执行多次等同于执行一次

幂等性一词来自数学。如果运算f满足f(f(x)) = f(x),就称其具有幂等性。

绝对值函数就是一个很好的例子。 |−5| = 5,并且 |5| = 5。

无论应用一次还是一百次,结果都相同。

应用到API上,就是这样。

相同请求发送1次还是N次,服务器状态都相同。

注意:并不是响应必须相同,而是服务器状态必须相同。即使第二次DELETE请求返回404,只要“资源不存在”这一服务器状态没有变化,它仍然具有幂等性。

日常例子是电梯按钮。即使连续按五次5楼按钮,电梯也只会去5楼一次。

相反,如果是“再上升5层”按钮,每按一次结果都会不同。前者具有幂等性,后者不具有幂等性。


为什么重要:网络必然会失败

幂等性为什么重要,用一个场景就能说明。

客户端发送支付请求。服务器处理了支付。

但是响应返回途中网络断了。

客户端无法知道,请求是在到达服务器前失败了(未支付),还是处理完成后只有响应丢失了(已支付)。

因为这两种情况看起来完全一样,都是超时。

使用相同幂等性密钥重试后仍只处理一次支付的时序图
响应丢失后,你无法知道支付是否成功。因此需要幂等性。

选择只有两个:放弃重试(支付可能未完成),或进行重试(如果已经完成,就会重复支付)。两种都很糟糕。

幂等性消除了这个两难。如果请求具有幂等性,“不确定就重新发送”始终是安全策略。

自动重试、消息队列的至少一次投递(at-least-once),以及客户端反复刷新,都不再需要担心。

这就是为什么每份分布式系统设计文档都会反复强调:“只对幂等操作设置重试。”


通过HTTP方法理解幂等性

HTTP规范(RFC 9110)明确规定了各方法的幂等性。

方法 幂等? 原因
GET O 查询不会改变状态
PUT O “替换为这个值”执行多少次都是相同的值
DELETE O “删除它”执行多少次都是已删除状态
POST X “创建新的”每调用一次就增加一个
PATCH 有条件 “将此字段设为X”具有幂等性,“加1”不具有幂等性

PUT和POST的对比是关键。PUT指定“结果状态”,因此具有幂等性;POST指示“行为”,因此不具有幂等性。

PATCH取决于具体内容。

浏览器通过后退返回POST页面时,会警告“要重新提交此表单吗?”,也是因为浏览器知道重新发送POST并不安全。


实用机制:幂等性密钥

但是注册账号、创建订单和支付本质上都是POST操作。无法避免非幂等操作。

因此出现了让非幂等请求变得幂等的机制:Idempotency-Key。

其工作方式很简单。

  1. 客户端为每个请求生成唯一密钥(通常是UUID),并将其放入请求头发送
  2. 如果密钥是第一次出现,服务器正常处理请求,并将响应与密钥一起保存
  3. 如果再次收到相同密钥,服务器不再处理,直接返回原样保存的响应

无论是重试还是双击,只要使用相同密钥,服务器就只处理一次。Stripe和Toss Payments等支付API实际支持此请求头,支付集成指南也将其作为必需项介绍。

客户端还有一个细节:“重试时要使用相同密钥”。

如果每次重试都生成新密钥,服务器会把它们视为不同请求。密钥应以“相同意图的请求”为单位生成。

支付终端只处理两个带有相同密钥的信封中的一个,并开出一张收据
收到相同幂等性密钥时,服务器只处理一次,并返回保存的响应

总结

  • 幂等性是指无论发送相同请求多少次,服务器状态都相同的性质(f(f(x)) = f(x))
  • 网络故障导致“无法确定是否完成”时,只有操作具有幂等性,重试才安全
  • HTTP:GET、PUT、DELETE具有幂等性,POST不具有幂等性,PATCH取决于内容
  • PUT指定结果状态,因此具有幂等性;POST指示行为,因此不具有幂等性
  • 使用Idempotency-Key让非幂等POST具备幂等性——相同密钥只处理一次
  • 重试时必须复用相同密钥。密钥以“相同意图的请求”为单位