Yuewen's Note

细说「幂等」

什么是幂等

幂等(idempotent)是一个数学 / 计算机学的概念。

一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同

幂等操作可以是一个数学函数、一个类方法、一个 API 调用 etc.

任意多次执行是指用相同参数,重复多次调用。

影响相同是指对业务状态的影响是一致的。这个概念比较抽象,用具体的例子说明。

API:/user/{username} GET
参数:neighbor_wang
影响:无,对业务数据未造成任何变更,多次执行都不会对业务数据造成任何影响

API:/user/update/{username} POST
参数:施瓦辛格格
影响:无,对业务数据造成变更,但多次执行结果相同,不会对业务数据造成非预期的影响(副作用)

API:/post/like/{post_id} POST
参数:1512211 (帖子ID)
影响:有,用户点赞 1 次,帖子只会被点赞 1 次。假如用户网络环境比较差,点赞后,未收到系统反馈。用户会多次对帖子进行点赞,而帖子就会被多次点赞。用户点赞多次执行与一次执行对系统状态的影响是不同的,所以这个 API 就是非幂等的。

那我们如何把上面的接口设计为幂等接口呢?

API:/post/like/{post_id} POST
参数:1512211 (帖子ID)、neighbor_wang
业务:用户点赞时,记录用户点赞行为,确保点赞表中存在一条记录(复合主键:post_id、username)
影响:无,用户多次对帖子点赞,帖子都只会被点赞一次,符合幂等的定义,属于幂等操作。


HTTP 幂等性

在HTTP/1.1规范中,幂等性的定义是:

是指一次和多次请求某一个资源应该具有同样的副作用。

通俗点说,就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。

说白了,就是同一个请求,发送一次和发送N次效果是一样的!

幂等性是分布式系统设计中十分重要的概念,而HTTP的分布式本质也决定了它在HTTP中具有重要地位。

可是,为什么说在分布式系统中需要「幂等性」呢?

典型案例

银行取款

假设有一个从账户取钱的远程 API, 我们暂时用类函数的方式记为:

1
bool withdraw(account_id, amount)

withdraw 的语义是从 account_id 对应的账户中扣除 amount 数额的钱;如果扣除成功则返回 true ,账户余额减少 amount;如果扣除失败则返回 false ,账户余额不变。

值得注意的是:和本地环境相比,我们不能轻易假设分布式环境的可靠性。

一种典型的情况是 withdraw 请求已经被服务器端正确处理,但服务器端的返回结果由于网络等原因被掉丢了,导致客户端无法得知处理结果。

如果是在网页上,一些不恰当的设计可能会使用户认为上一次操作失败了,然后刷新页面,这就导致了 withdraw 被调用两次,账户也被多扣了一次钱 (客户端行为不可预知,不能依赖客户端行为) 。如「图 1」所示:

图 1

一种轻量级的解决方案是把 API 设计为幂等性的。我们可以通过一些技巧把 withdraw 变成幂等的。比如:

1
2
int create_ticket()
bool idempotent_withdraw(ticket_id, account_id, amount)

create_ticket 的语义是获取一个服务器端生成的唯一的处理号 ticket_id,它将用于标识后续的操作。

idempotent_withdrawwithdraw 的区别在于关联了一个 唯一标识这次操作ticket_id,一个 ticket_id 表示的操作至多只会被处理一次,每次调用都将返回第一次调用时的处理结果。

这样,idempotent_withdraw 就符合幂等性了,客户端就可以放心地多次调用。

基于「幂等性」的解决方案中,一个完整的取钱流程被分解成了两个步骤:

  1. 调用 create_ticket() 获取 ticket_id
  2. 调用 idempotent_withdraw(ticket_id, account_id, amount)。虽然 create_ticket 不是幂等的,但在这种设计下,它对系统状态的影响可以忽略,加上 idempotent_withdraw 是幂等的,所以任何一步由于网络等原因失败或超时,客户端都可以重试,直到获得结果。如「图 2」所示:

图 2

幂等设计的优势在于它的轻量级,容易适应异构环境,以及性能和可用性方面。在某些性能要求比较高的应用,幂等设计往往是唯一的选择。

扩展案例

如何防范表单 (POST) 重复提交

HTTP POST 操作既不是安全的,也不是幂等的(至少在HTTP规范里没有保证)。当我们因为反复刷新浏览器导致多次提交表单,多次发出同样的POST请求,导致远端服务器重复创建出了资源。

所以,对于电商应用来说,第一对应的后端 WebService 一定要做到幂等性,第二服务器端收到 POST 请求,在操作成功后必须302跳转到另外一个页面,这样即使用户刷新页面,也不会重复提交表单。

把分布式事务分解为具有幂等性的异步消息处理

电商的很多业务,考虑更多的是 BASE(即Basically Available、Soft state、和Eventually consistent),而不是 ACID(Atomicity、Consistency、Isolation和 Durability)。即为了满足高负载的用户访问,我们可以容忍短暂的数据不一致。那怎么做呢?

  1. 不做分布式事务,代价太大
  2. 不一定需要实时一致性,只需要保证最终的一致性即可
  3. 通过状态机和严格的有序操作,来最大限度地降低不一致性
  4. 最终一致性(Eventually Consistent)通过异步事件做到

如果消息具有操作幂等性,也就是一个消息被应用多次与应用一次产生的效果是一样的话,那么把不需要同步执行的事务交给异步消息推送和订阅者集群来处理即可。假如消息处理失败,那么就消息重播,由于幂等性,应用多次也能产生正确的结果。

实际情况下,消息很难具有幂等性,解决方法是使用另一个表记录已经被成功应用的消息,即消息队列和消息应用状态表一起来解决问题。


REF: 理解HTTP幂等性