API 设计 — 如何设计稳定可预测的 API (谈幂等性)?

2024年10月17日

💎 加入 E+ 成長計畫 與超過 500+ 位軟體工程師一同在社群中成長,並且獲得更多的軟體工程學習資源

在之前的文章 API 设计 — 好的 API 设计有什么特点? 中,我们讨论了好的 API 有什么特点,其中有提到两点是我们认为需要更多版面来谈的,分别是可相容性可预测性。之所以说这两点特别重要,是因为当没有处理好,会导致后续很多问题。

在这篇文章中,我们会针对「可预测性」来讨论。(完整内容收录在 E+ 成长计划当中。)

可预测性:让 API 稳定可预测

在前后端的世界中,因为沟通不会随时都是稳定的,例如客户端的装置可能出问题、网路可能中间突然断掉一小段时间,所以在前后端用 API 沟通时,不能只考虑正常的情境 (俗称 happy path),而是要进一步去考虑出特别的问题时该怎么处理 (俗称 edge case)。当能够去处理各类极端状况,API 与整个系统将能够更稳定。

举例来说,当今天你用一个电子信箱产品,在编写完草稿后,按下储存按钮,不管按一次,还是按十次,都是执行储存当下的状态,不会因为你按了十次,中间网路不稳,就存成十份不同的草稿。如果真的是这样的话,使用者体验会不太好。

又或者今天假如使用者在电商网站结帐时,中间网路有一度不稳,使用者等了一下看页面没反应,以为自己没按成功,所以多按了一次结帐,这时不会要让使用者付两次款。如果变得要付两次款,那肯定没有使用者会想用这个电商的产品。

要能够做到稳定,幂等 (idempotent) 是很关键的要点。所谓的幂等性,是指 API 的呼叫或者操作,不论做多少次,都会是相同的结果;或者换个角度说要做到不论请求几次,API 都不会产生副作用。当能够做到幂等性,就能够确保在重试时,不论重试几次,都能确保只有执行一次,这样能避免当遇到各类状况,导致的不必要重复。

事实上,过去业界就有因为没有处理好 API 幂等,导致造成重大事故的案例。

具体来说,先前 Uber 支付组的工程经理 Gergely Orosz 就曾公开分享,当年 Uber Eats 在印度有个重大事故,是某段时间内,绑定印度最大支付商之一 Paytm 的使用者,即使 Paytm 帐户没有余额,也可以无限地在 Uber Eats 上下单。

Gergely 在分享中谈到,会出现这个事故,是当年 Paytm 的 API 做了改动。在改动前,Paytm 的 API 一直是维持幂等的,所以 Uber 的支付团队在串接时,就预设 API 是幂等的,没有多做处理。

然而 Paytm 在那次看似无害的改动中,没有维持 API 的幂等性,这造成的问题是,当 Uber 呼叫 Paytm 的 API 时,第一次因为使用者的余额不足所以回传原本预期的错误,但这时如果使用者再下一次单,Paytm 会回传另一种错误讯息。

两次回传不同的错误讯息,看似很无害,但是偏偏因为第二种错误讯息原本 Uber 团队不知道,所以没处理,因此在 Uber 端就让这种下单通过。而当使用者发现没余额时,只要按两次就变得能下单成功,当时印度各大学迅速传开,让 Uber Eats 在短时间被大量下免费的单,而这造成的商业损失非常可观。

如何让 API 有幂等性?

相信看完上面的故事,读者们已经意识到幂等性的重要。如果 Paytm 的 API 在遇到余额不足,是稳定回传 Uber 端可以处理的错误讯息。

这时下个问题会是如何让 API 有幂等性?

以 RESTful API 来说,有些请求相对不用担心幂等性问题。举例来说,GET 请求就是,因为假如某个资料存在伺服器,不论请求几次,资料没变的状况下,就会都拿到一样的资料。

PUT 也是,因为 PUT 是一次修改整个资源,假如有多个请求送来,就以最后送到的请求即可。同样地 DELETE 删除某个资源后,就没有该资源,多发几个过来的结果都是该资源被删除,所以也是幂等。

然而,我们很常用的 POST 请求会是相对需要特别处理的。就像电商下订单时的支付,通常会是用 POST 请求。而最常见的幂等处理方式会是加上幂等钥 (idempotent key)。所谓的幂等钥,是一个独特的 id,让伺服器端知道这个请求已经被处理过了。

所以如果有网路中断,或者使用者快速连击,当同一个请求带着相同的幂等钥,伺服器端就知道不用再处理该请求。在系统设计中,遇到追问如何在分散式系统中,避免请求被重复处理,幂等钥是最基本一定要想到的解法。

举例来说,全球支付 API 龙头之一的 Stripe,在 API 设计中,就有幂等钥的栏位:

Stripe API 文件
Stripe API 文件

具体来说,假如要呼叫 CreatePayment 的 API,客户端可以先产生一组幂等钥 (例如用 uuid 来产生),这时如果使用者重复点击,因为带着的是同一组幂等钥,所以伺服器端知道已经处理过,就不会重新处理这个支付请求。

先前 Stripe 有一篇《Designing robust and predictable APIs with idempotency》技术文,深入浅出地谈了如何透过幂等钥来提高 API 可预测性,非常推荐一读。

阅读更多

如果你对于「如何设计好 API」这主题感兴趣,我们在 E+ 有写更深入详细的内容,包含可相容性、如何做好向后相容 (backward compatible)。有兴趣的读者,欢迎加入 E+ 成长计划。

本文为 E+ 成长计划的深度内容,截取前三分之一开放免费阅读。欢迎加入 E+ 成长计划阅读完整版本 (点此了解 E+ 的详细介绍)


API 系列文

🧵 如果你想收到最即時的內容更新,可以在 FacebookInstagram 上追蹤我們