轉(zhuǎn)載自 Programming.log - a place to keep my thoughts on programming
基于HTTP協(xié)議的Web API是時(shí)下最為流行的一種分布式服務(wù)提供方式。無(wú)論是在大型互聯(lián)網(wǎng)應(yīng)用還是企業(yè)級(jí)架構(gòu)中,我們都見(jiàn)到了越來(lái)越多的SOA或RESTful的Web API。為什么Web API如此流行呢?我認(rèn)為很大程度上應(yīng)歸功于簡(jiǎn)單有效的HTTP協(xié)議。HTTP協(xié)議是一種分布式的面向資源的網(wǎng)絡(luò)應(yīng)用層協(xié)議,無(wú)論是服務(wù)器端提供Web服務(wù),還是客戶端消費(fèi)Web服務(wù)都非常簡(jiǎn)單。再加上瀏覽器、Javascript、AJAX、JSON以及HTML5等技術(shù)和工具的發(fā)展,互聯(lián)網(wǎng)應(yīng)用架構(gòu)設(shè)計(jì)表現(xiàn)出了從傳統(tǒng)的PHP、JSP、ASP.NET等服務(wù)器端動(dòng)態(tài)網(wǎng)頁(yè)向Web API + RIA(富互聯(lián)網(wǎng)應(yīng)用)過(guò)渡的趨勢(shì)。Web API專注于提供業(yè)務(wù)服務(wù),RIA專注于用戶界面和交互設(shè)計(jì),從此兩個(gè)領(lǐng)域的分工更加明晰。在這種趨勢(shì)下,Web API設(shè)計(jì)將成為服務(wù)器端程序員的必修課。然而,正如簡(jiǎn)單的Java語(yǔ)言并不意味著高質(zhì)量的Java程序,簡(jiǎn)單的HTTP協(xié)議也不意味著高質(zhì)量的Web API。要想設(shè)計(jì)出高質(zhì)量的Web API,還需要深入理解分布式系統(tǒng)及HTTP協(xié)議的特性。
冪等性定義
本文所要探討的正是HTTP協(xié)議涉及到的一種重要性質(zhì):冪等性(Idempotence)。在HTTP/1.1規(guī)范中冪等性的定義是:
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方法的冪等性是指一次和多次請(qǐng)求某一個(gè)資源應(yīng)該具有同樣的副作用。冪等性屬于語(yǔ)義范疇,正如編譯器只能幫助檢查語(yǔ)法錯(cuò)誤一樣,HTTP規(guī)范也沒(méi)有辦法通過(guò)消息格式等語(yǔ)法手段來(lái)定義它,這可能是它不太受到重視的原因之一。但實(shí)際上,冪等性是分布式系統(tǒng)設(shè)計(jì)中十分重要的概念,而HTTP的分布式本質(zhì)也決定了它在HTTP中具有重要地位。
分布式事務(wù) vs 冪等設(shè)計(jì)
為什么需要冪等性呢?我們先從一個(gè)例子說(shuō)起,假設(shè)有一個(gè)從賬戶取錢的遠(yuǎn)程API(可以是HTTP的,也可以不是),我們暫時(shí)用類函數(shù)的方式記為:
bool withdraw(account_id, amount)
withdraw的語(yǔ)義是從account_id對(duì)應(yīng)的賬戶中扣除amount數(shù)額的錢;如果扣除成功則返回true,賬戶余額減少amount;如果扣除失敗則返回false,賬戶余額不變。值得注意的是:和本地環(huán)境相比,我們不能輕易假設(shè)分布式環(huán)境的可靠性。一種典型的情況是withdraw請(qǐng)求已經(jīng)被服務(wù)器端正確處理,但服務(wù)器端的返回結(jié)果由于網(wǎng)絡(luò)等原因被掉丟了,導(dǎo)致客戶端無(wú)法得知處理結(jié)果。如果是在網(wǎng)頁(yè)上,一些不恰當(dāng)?shù)脑O(shè)計(jì)可能會(huì)使用戶認(rèn)為上一次操作失敗了,然后刷新頁(yè)面,這就導(dǎo)致了withdraw被調(diào)用兩次,賬戶也被多扣了一次錢。如圖1所示:
這個(gè)問(wèn)題的解決方案一是采用分布式事務(wù),通過(guò)引入支持分布式事務(wù)的中間件來(lái)保證withdraw功能的事務(wù)性。分布式事務(wù)的優(yōu)點(diǎn)是對(duì)于調(diào)用者很簡(jiǎn)單,復(fù)雜性都交給了中間件來(lái)管理。缺點(diǎn)則是一方面架構(gòu)太重量級(jí),容易被綁在特定的中間件上,不利于異構(gòu)系統(tǒng)的集成;另一方面分布式事務(wù)雖然能保證事務(wù)的ACID性質(zhì),而但卻無(wú)法提供性能和可用性的保證。
另一種更輕量級(jí)的解決方案是冪等設(shè)計(jì)。我們可以通過(guò)一些技巧把withdraw變成冪等的,比如:
int create_ticket()
bool idempotent_withdraw(ticket_id, account_id, amount)
create_ticket的語(yǔ)義是獲取一個(gè)服務(wù)器端生成的唯一的處理號(hào)ticket_id,它將用于標(biāo)識(shí)后續(xù)的操作。idempotent_withdraw和withdraw的區(qū)別在于關(guān)聯(lián)了一個(gè)ticket_id,一個(gè)ticket_id表示的操作至多只會(huì)被處理一次,每次調(diào)用都將返回第一次調(diào)用時(shí)的處理結(jié)果。這樣,idempotent_withdraw就符合冪等性了,客戶端就可以放心地多次調(diào)用。
基于冪等性的解決方案中一個(gè)完整的取錢流程被分解成了兩個(gè)步驟:1.調(diào)用create_ticket()獲取ticket_id;2.調(diào)用idempotent_withdraw(ticket_id, account_id, amount)。雖然create_ticket不是冪等的,但在這種設(shè)計(jì)下,它對(duì)系統(tǒng)狀態(tài)的影響可以忽略,加上idempotent_withdraw是冪等的,所以任何一步由于網(wǎng)絡(luò)等原因失敗或超時(shí),客戶端都可以重試,直到獲得結(jié)果。如圖2所示:
和分布式事務(wù)相比,冪等設(shè)計(jì)的優(yōu)勢(shì)在于它的輕量級(jí),容易適應(yīng)異構(gòu)環(huán)境,以及性能和可用性方面。在某些性能要求比較高的應(yīng)用,冪等設(shè)計(jì)往往是唯一的選擇。
HTTP的冪等性
HTTP協(xié)議本身是一種面向資源的應(yīng)用層協(xié)議,但對(duì)HTTP協(xié)議的使用實(shí)際上存在著兩種不同的方式:一種是RESTful的,它把HTTP當(dāng)成應(yīng)用層協(xié)議,比較忠實(shí)地遵守了HTTP協(xié)議的各種規(guī)定;另一種是SOA的,它并沒(méi)有完全把HTTP當(dāng)成應(yīng)用層協(xié)議,而是把HTTP協(xié)議作為了傳輸層協(xié)議,然后在HTTP之上建立了自己的應(yīng)用層協(xié)議。本文所討論的HTTP冪等性主要針對(duì)RESTful風(fēng)格的,不過(guò)正如上一節(jié)所看到的那樣,冪等性并不屬于特定的協(xié)議,它是分布式系統(tǒng)的一種特性;所以,不論是SOA還是RESTful的Web API設(shè)計(jì)都應(yīng)該考慮冪等性。下面將介紹HTTP GET、DELETE、PUT、POST四種主要方法的語(yǔ)義和冪等性。
HTTP GET方法用于獲取資源,不應(yīng)有副作用,所以是冪等的。比如:GET http://www.bank.com/account/123456,不會(huì)改變資源的狀態(tài),不論調(diào)用一次還是N次都沒(méi)有副作用。請(qǐng)注意,這里強(qiáng)調(diào)的是一次和N次具有相同的副作用,而不是每次GET的結(jié)果相同。GET http://www.news.com/latest-news這個(gè)HTTP請(qǐng)求可能會(huì)每次得到不同的結(jié)果,但它本身并沒(méi)有產(chǎn)生任何副作用,因而是滿足冪等性的。
HTTP DELETE方法用于刪除資源,有副作用,但它應(yīng)該滿足冪等性。比如:DELETE http://www.forum.com/article/4231,調(diào)用一次和N次對(duì)系統(tǒng)產(chǎn)生的副作用是相同的,即刪掉id為4231的帖子;因此,調(diào)用者可以多次調(diào)用或刷新頁(yè)面而不必?fù)?dān)心引起錯(cuò)誤。
比較容易混淆的是HTTP POST和PUT。POST和PUT的區(qū)別容易被簡(jiǎn)單地誤認(rèn)為“POST表示創(chuàng)建資源,PUT表示更新資源”;而實(shí)際上,二者均可用于創(chuàng)建資源,更為本質(zhì)的差別是在冪等性方面。在HTTP規(guī)范中對(duì)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所對(duì)應(yīng)的URI并非創(chuàng)建的資源本身,而是資源的接收者。比如:POST http://www.forum.com/articles的語(yǔ)義是在http://www.forum.com/articles下創(chuàng)建一篇帖子,HTTP響應(yīng)中應(yīng)包含帖子的創(chuàng)建狀態(tài)以及帖子的URI。兩次相同的POST請(qǐng)求會(huì)在服務(wù)器端創(chuàng)建兩份資源,它們具有不同的URI;所以,POST方法不具備冪等性。而PUT所對(duì)應(yīng)的URI是要?jiǎng)?chuàng)建或更新的資源本身。比如:PUT http://www.forum/articles/4231的語(yǔ)義是創(chuàng)建或更新ID為4231的帖子。對(duì)同一URI進(jìn)行多次PUT的副作用和一次PUT是相同的;因此,PUT方法具有冪等性。
在介紹了幾種操作的語(yǔ)義和冪等性之后,我們來(lái)看看如何通過(guò)Web API的形式實(shí)現(xiàn)前面所提到的取款功能。很簡(jiǎn)單,用POST /tickets來(lái)實(shí)現(xiàn)create_ticket;用PUT /accounts/account_id/ticket_id&amount=xxx來(lái)實(shí)現(xiàn)idempotent_withdraw。值得注意的是嚴(yán)格來(lái)講amount參數(shù)不應(yīng)該作為URI的一部分,真正的URI應(yīng)該是/accounts/account_id/ticket_id,而amount應(yīng)該放在請(qǐng)求的body中。這種模式可以應(yīng)用于很多場(chǎng)合,比如:論壇網(wǎng)站中防止意外的重復(fù)發(fā)帖。
總結(jié):
上面簡(jiǎn)單介紹了冪等性的概念,用冪等設(shè)計(jì)取代分布式事務(wù)的方法,以及HTTP主要方法的語(yǔ)義和冪等性特征。其實(shí),如果要追根溯源,冪等性是數(shù)學(xué)中的一個(gè)概念,表達(dá)的是N次變換與1次變換的結(jié)果相同,有興趣的讀者可以從Wikipedia上進(jìn)一步了解。
參考文獻(xiàn)
RFC 2616, Hypertext Transfer Protocol -- HTTP/1.1, Method Definitions
The Importance of Idempotence
Stackoverflow - PUT vs POST in REST