转自:http://coolshell.cn/articles/4787.html

HTTP 幂等性概念和应用

[ 感谢 Todd 同学 投递本文 ]

基于 HTTP 协议的 Web API 是时下最为流行的一种分布式服务提供方式。无论是在大型互联网应用还是企业级架构中,我们都见到了越来越多的 SOA 或 RESTful 的 Web API。为什么 Web API 如此流行呢?我认为很大程度上应归功于简单有效的 HTTP 协议。HTTP 协议是一种分布式的面向资源的网络应用层协议,无论是服务器端提供 Web 服务,还是客户端消费 Web 服务都非常简单。再加上浏览器、Javascript、AJAX、JSON 以及 HTML5 等技术和工具的发展,互联网应用架构设计表现出了从传统的
PHP、JSP、ASP.NET 等服务器端动态网页向 Web API + RIA(富互联网应用)过渡的趋势。Web API 专注于提供业务服务,RIA 专注于用户界面和交互设计,从此两个领域的分工更加明晰。在这种趋势下,Web API 设计将成为服务器端程序员的必修课。然而,正如简单的 Java 语言并不意味着高质量的 Java 程序,简单的 HTTP 协议也不意味着高质量的 Web API。要想设计出高质量的 Web API,还需要深入理解分布式系统及 HTTP 协议的特性。

幂等性定义

本文所要探讨的正是 HTTP 协议涉及到的一种重要性质:幂等性(Idempotence)。在 HTTP/1.1 规范中幂等性的定义是:

Methods can also have the property of “idempotence” in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request.

从定义上看,HTTP 方法的幂等性是指一次和多次请求某一个资源应该具有同样的副作用。幂等性属于语义范畴,正如编译器只能帮助检查语法错误一样,HTTP 规范也没有办法通过消息格式等语法手段来定义它,这可能是它不太受到重视的原因之一。但实际上,幂等性是分布式系统设计中十分重要的概念,而 HTTP 的分布式本质也决定了它在 HTTP 中具有重要地位。

分布式事务 vs 幂等设计

为什么需要幂等性呢?我们先从一个例子说起,假设有一个从账户取钱的远程 API(可以是 HTTP 的,也可以不是),我们暂时用类函数的方式记为

bool withdraw(account_id, amount); 

withdraw 的语义是从 account_id 对应的账户中扣除 amount 数额的钱;如果扣除成功则返回 true,账户余额减少 amount;如果扣除失败则返回 false,账户余额不变。值得注意的是:和本地环境相比,我们不能轻易假设分布式环境的可靠性。一种典型的情况是 withdraw 请求已经被服务器端正确处理,但服务器端的返回结果由于网络等原因被丢掉了,导致客户端无法得知处理结果。如果是在网页上,一些不恰当的设计可能会使用户认为上一次操作失败了,然后刷新页面,这就导致了 withdraw
被调用两次,账户也被多扣了一次钱。如图 1 所示:

图 1

这个问题的解决方案一是采用分布式事务,通过引入支持分布式事务的中间件来保证 withdraw 功能的事务性。分布式事务的优点是对于调用者很简单,复杂性都交给了中间件来管理。缺点则是一方面架构太重量级,容易被绑在特定的中间件上,不利于异构系统的集成;另一方面分布式事务虽然能保证事务的 ACID 性质,而但却无法提供性能和可用性的保证。

另一种更轻量级的解决方案是幂等设计。上面的 withdraw 显然不满足幂等性,但我们可以一些技巧将它变成幂等的,比如:

int create_ticket();

bool idempotent_withdraw(ticket_id, account_id, amount);

create_ticket 的语义是获取一个服务器端生成的唯一的处理号 ticket_id,它将用于标识后续的操作。idempotent_withdraw 和 withdraw 的区别在于关联了一个 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

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

HTTP 的幂等性

HTTP 协议本身是一种面向资源的应用层协议,但对 HTTP 协议的使用实际上存在着两种不同的方式:一种是 RESTful 的,它把 HTTP 当成应用层协议,比较忠实地遵守了 HTTP 协议的各种规定;另一种是 SOA 的,它并没有完全把 HTTP 当成应用层协议,而是把 HTTP 协议作为了传输层协议,然后在 HTTP 之上建立了自己的应用层协议。本文所讨论的 HTTP 幂等性主要针对 RESTful 风格的,不过正如上一节所看到的那样,幂等性并不属于特点的协议,它是分布式系统的一种特性;所以,不论是
SOA 还是 RESTful 的 Web API 设计都应该考虑幂等性。下面将介绍 HTTP GET、DELETE、PUT、POST 四种主要方法的语义和幂等性。

HTTP GET 方法用于获取资源,不应有副作用,所以是幂等的。比如:GET http://www.bank.com/account/123456,不会改变资源的状态,不论调用一次还是 N 次都没有副作用。请注意,这里强调的是一次和 N 次具有相同的副作用,而不是每次 GET 的结果相同。GET http://www.news.com/latest-news 这个 HTTP 请求可能会每次得到不同的结果,但它本身并没有产生任何副作用,因而是满足幂等性的。

HTTP DELETE 方法用于删除资源,有副作用,但它应该满足幂等性。比如:DELETE http://www.forum.com/article/4231,调用一次和 N 次对系统产生的副作用是相同的,即删掉 id 为 4231 的帖子;因此,调用者可以多次调用或刷新页面而不必担心引入错误。

比较容易混淆的是 HTTP POST 和 PUT。POST 和 PUT 的区别容易被简单地误认为“ POST 表示创建资源,PUT 表示更新资源”;而实际上,二者均可用于创建资源,更为本质的差别是在幂等性方面。在HTTP规范中对 POST 和 PUT 是这样定义的:

The POST method is used to request that the origin server accept the entity enclosed in the request as a new subordinate of the resource identified by the Request-URI in the Request-Line. …… If a resource has been created
on the origin server, the response SHOULD be 201 (Created) and contain an entity which describes the status of the request and refers to the new resource, and a Location header.

The PUT method requests that the enclosed entity be stored under the supplied Request-URI. If the Request-URI refers to an already existing resource, the enclosed entity SHOULD be considered as a modified version of the one
residing on the origin server. If the Request-URI does not point to an existing resource, and that URI is capable of being defined as a new resource by the requesting user agent, the origin server can create the resource with that URI.

POST 所对应的 URI 并非创建的资源本身,而是资源的接收者。比如:POST http://www.forum.com/articles 的语义是在 http://www.forum.com/articles 下创建一篇帖子,HTTP 响应中应包含帖子的创建状态以及帖子的 URI。两次相同的 POST 请求会在服务器端创建两份资源,它们具有不同的 URI;所以,POST 方法不具备幂等性。

而 PUT 所对应的 URI 是要创建或更新的资源本身。比如:PUT http://www.forum/articles/4231 的语义是创建或更新 ID 为 4231 的帖子。对同一 URI 进行多次 PUT 的副作用和一次 PUT 是相同的;因此,PUT 方法具有幂等性。

在介绍了几种操作的语义和幂等性之后,我们来看看如何通过 Web API 的形式实现前面所提到的取款功能。很简单,用 POST /tickets 来实现 create_ticket;用 PUT  /accounts/account_id/ticket_id&amount=xxx 来实现 idempotent_withdraw。值得注意的是严格来讲 amount 参数不应该作为 URI 的一部分,真正的 URI 应该是 /accounts/account_id/ticket_id,而 amount 应该放在请求的
body 中。这种模式可以应用于很多场合,比如:论坛网站中防止意外的重复发帖。

总结

上面简单介绍了幂等性的概念,用幂等设计取代分布式事务的方法,以及 HTTP 主要方法的语义和幂等性特征。其实,如果要追根溯源,幂等性是数学中的一个概念,表达的是 N 次变换与 1 次变换的结果相同,有兴趣的读者可以从 Wikipedia 上进一步了解。

参考

RFC 2616, Hypertext Transfer Protocol — HTTP/1.1, Method Definitions

The Importance of Idempotence

(转载本站文章请注明作者和出处 酷 壳 – CoolShell.cn ,请勿用于任何商业用途)

[置顶] HTTP 幂等性概念和应用的更多相关文章

  1. JavaScript中hoisting(悬置/置顶解析/预解析) 实例解释,全局对象,隐含的全局概念

    JavaScript中hoisting(悬置/置顶解析/预解析) 实例解释,全局对象,隐含的全局概念 <html> <body> <script type="t ...

  2. 变量声明置顶规则、函数声明及函数表达式和函数的arguments属性初始化

    一.变量声明和变量赋值: if (!("a" in window)) { ; } alert(a);//a为? 你可能认为alert出来的结果是1,然后实际结果是“undefine ...

  3. win api 窗口操作-窗口置顶与寻找与激活

    https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowpos https://docs.micr ...

  4. 在UWP中页面滑动导航栏置顶

    最近在研究掌上英雄联盟,主要是用来给自己看新闻,顺便copy个界面改一下段位装装逼,可是在我copy的时候发现这个东西 当你滑动到一定距离的时候导航栏会置顶不动,这个特性在微博和淘宝都有,我看了@ms ...

  5. WinFrom窗体始终置顶

    调用WindowsAPI使窗体始终保持置顶效果,不被其他窗体遮盖: [DllImport("user32.dll", CharSet = CharSet.Auto)] privat ...

  6. winform窗体置顶

    winform窗体置顶 金刚 winform 置顶 今天做了一个winform小工具.需要设置置顶功能. 网上找了下,发现百度真的很垃圾... 还是必应靠谱些. 找到一个可以链接. https://s ...

  7. 自定义置顶TOP按钮

    简述一下,分为三个步骤: 1. 添加Html代码 2. 调整Css样式 3. 添加Jquery代码 具体代码如下: <style type="text/css"> #G ...

  8. ahk之路:利用ahk在window7下实现窗口置顶

    操作系统:win7 64位 ahk版本:autohotkey_L1.1.24.03 今天安装了AutoHotkey_1.1.24.03.SciTE.PuloversMacroCreator,重新开始我 ...

  9. Qt中让Qwidget置顶的方法

    一般来是说窗体置顶和取消只要        setWindowFlags(Qt::WindowStaysOnTopHint);        setWindowFlags(Qt::Widget); 要 ...

随机推荐

  1. Spark MLlib之线性回归源代码分析

    1.理论基础 线性回归(Linear Regression)问题属于监督学习(Supervised Learning)范畴,又称分类(Classification)或归纳学习(Inductive Le ...

  2. Java中的事务

    Java中的事务 学习了:https://www.cnblogs.com/chengpeng15/p/5802930.html 膜拜一下 org 分为三类:jdbc事务.jta事务.容器事务:

  3. Win8下建立shortcut到開始界面

    在win8前建立開始菜单都非常easy,但到win8就有点不一样了.它的開始菜单是metro风格的.以下我们来看下详细的实现代码.有兴趣的朋友能够自己測试下,它的作用是设置shortcut到metro ...

  4. BZOJ 4517: [Sdoi2016]排列计数 错排+逆元

    4517: [Sdoi2016]排列计数 Description 求有多少种长度为 n 的序列 A,满足以下条件: 1 ~ n 这 n 个数在序列中各出现了一次 若第 i 个数 A[i] 的值为 i, ...

  5. HDU3535 AreYouBusy 混合背包

    题目大意 给出几组物品的体积和价值,每组分为三种:0.组内物品至少选一个:1.组内物品最多选一个:2.组内物品任意选.给出背包容量,求所能得到的最大价值. 注意 仔细审题,把样例好好看完了再答题,否则 ...

  6. 解决xftp失去链接需要重新链接问题。

    XFTP 失去连接需要重新连接 打开 Xftp 主程序. 在顶部菜单[文件] – [属性], 打开[默认会话属性]窗口,点击[选项],在连接部分选择勾选"发送保持活动状态消息(s)" ...

  7. Swift - 获取当前时间的时间戳(时间戳与时间互相转换)

    (本文代码已升级至Swift3) 1,时间戳 时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数. 2,获取当前时间的时 ...

  8. VMware 安装LINUX系统(一)

    我用的是WORKSTATION 15 PRO https://www.vmware.com/asean/products/workstation-pro/ 1.安装LINUX 打开Vmware,点击创 ...

  9. BZOJ 3522 DFS+DP

    思路: f[]表示选1个点的 g[]表示选2个点的 dp一下 ans+=(ll)g[k]*deep[k]; g[k]+=(ll)f[k]*deep[k]; f[k]+=deep[k]; 听说有O(n) ...

  10. 【Oracle】重置参数

    单实例中: alter system reset parameter <scope=memory|spfile|both>: --memory|spfile|both,选其一 集群环境中: ...