返回首页

幂等性和副作用

用户狂点上传按钮该怎么处理——幂等 API 设计的思路。

发布 2019年3月22日 标签 #api-design #http #rest

~/posts/idempotence-and-side-effects $ cat post.md

/ 语言 EN / 中文
/ 主题 / /

用户就是要狂点

用户的行为不可预知。同一个操作被反复点几十次是非常正常的。

假设有这样一个场景:用户发 POST 请求新建一篇文章。API 大概会是 POST https://domain.com/article/create,或者更 REST 一点的纯资源风格 POST https://domain.com/article

用户在这件事上能做的”文章”挺多——比如写好之后疯狂点上传按钮 100 下。

无副作用的函数

函数式编程的吸引力之一就是无副作用:相同的输入产生相同的输出,调用一次或调用一百次结果一样,前面的结果丢掉只看最后一次也行。

把这个思路套到 POST 上:能不能让请求”在某些输入相同的时候”返回相同结果、并且没有副作用?显然不行——“创建一篇文章”这件事本身就是副作用,整个 API 存在的意义就是这个副作用。

中间件

办法总是有的。一个常见思路是中间件:用户的请求都打在中间件上,中间件用分布式事务的方式处理实际写入。当中间件检测到用户提交的是”同一篇”文章时拦截掉,用户点多少次都不会重复创建。

弊端:

  • 判断”是不是同一篇”在技术上是模糊的。完全相同的字面文本算同一篇,那改了一个字呢?几个字呢?这是没法直接回答的哲学问题。
  • 不是每个服务都有必要为此引入分布式事务 + 中间件这一套复杂架构。

退一步看,核心其实是请求的语义本身——这个 POST 写得太笼统,按”无副作用”的标准看,传入根本不足以判断重复。

幂等性(Idempotence)

幂等的定义:N 次变换的结果和 1 次变换的结果相同。即同样的请求被执行一次或多次,产生相同的副作用。

设计面向用户的系统时,幂等性是个重要概念。HTTP 协议是幂等设计天然的舞台。

HTTP 方法语义

经典面试题——GET 和 POST 有什么区别?其中一条:GET 不应该产生副作用,同一个 GET 请求在资源没变的前提下应当永远返回相同的结果。

同样的思路可以用到其它方法。容易混淆的是 POST 和 PUT。有种说法是”PUT 用于更新、POST 用于创建”——单纯按动作分类没有意义,POST 实际上可以做任何事。按语义看:PUT 应该指向一个具体的资源——给一个 URI,PUT 操作把资源的状态替换/更新为 body 里的内容。重复 PUT 同样的 body 应该得到相同的结果,是幂等的。

解法

回到例子。不引入中间件的情况下,可以把”创建文章”拆成两步:

  1. POST https://domain.com/article/create——返回 201 和一个新的文章 ID,但此时文章是空的,没有任何内容、不出现在用户的文章列表里;如果一段时间内没填内容就自动销毁。
  2. PUT https://domain.com/article/{id}——往这个 ID 对应的位置填内容。

用户狂点上传按钮的话,每次得到的是不同的 ID,但只有”真正填上内容”的那一篇会留下来。或者更进一步:把第二步的 PUT 也设计成幂等——同样的内容 PUT 多次,结果都是同一篇。

返回首页