程式設計冪等設計:資料最終一致性的保證

冪等定義

冪等(idempotent、idempotence)是一個數學與計算機學概念,常見於抽象代數中。
在程式設計中.一個冪等操作的特點是其任意多次執行所產生的影響均與一次執行的影響相同。冪等函式,或冪等方法,是指可以使用相同引數重複執行,並能獲得相同結果的函式。這些函式不會影響系統狀態,也不用擔心重複執行會對系統造成改變。例如,“setTrue()”函式就是一個冪等函式,無論多次執行,其結果都是一樣的.更復雜的操作冪等保證是利用唯一交易號(流水號)實現.

什麼是冪等性

冪等性的數學表達:f(f(x)) = f(x)。
冪等性是系統介面對外的一種承諾。
冪等性指的是,使用相同引數對同一資源重複呼叫某個介面的結果與呼叫一次的結果相同。
冪等性的一個實現是,使你的介面必須返回 0(成功),即使這時資源或動作已經停止並且無工作要完成。

理解:冪等性是系統的介面對外一種承諾(而不是實現),承諾只要呼叫介面成功,外部多次呼叫對系統的影響是一致的。宣告為冪等的介面會認為外部呼叫失敗是常態,並且失敗之後必然會有重試。介面冪等性,只要保證介面內的邏輯不涉及介面外的物件狀態累積或變遷即可。

HTTP冪等理解

基於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語言並不意味著高質量的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的,也可以不是),我們暫時用類函式的方式記為:

boolean withdraw(account_id, amount);//非冪等取款操作

withdraw的語義是從account_id對應的賬戶中扣除amount數額的錢;如果扣除成功則返回true,賬戶餘額減少amount;如果扣除失敗則返回false,賬戶餘額不變。值得注意的是:和本地環境相比,我們不能輕易假設分散式環境的可靠性。一種典型的情況是withdraw請求已經被伺服器端正確處理,但伺服器端的返回結果由於網路等原因被掉丟了,導致客戶端無法得知處理結果。如果是在網頁上,一些不恰當的設計可能會使使用者認為上一次操作失敗了,然後重新整理頁面,這就導致了withdraw被呼叫兩次,賬戶也被多扣了一次錢。如圖1所示:

圖1

這個問題的解決方案一是採用分散式事務,通過引入支援分散式事務的中介軟體來保證withdraw功能的事務性。分散式事務的優點是對於呼叫者很簡單,複雜性都交給了中介軟體來管理。缺點則是一方面架構太重量級,容易被綁在特定的中介軟體上,不利於異構系統的整合;另一方面分散式事務雖然能保證事務的ACID性質,而但卻無法提供效能和可用性的保證。

另一種更輕量級的解決方案是冪等設計。我們可以通過一些技巧把withdraw變成冪等的,比如:

int create_ticket();// 獲取請求序列ticket_id(實際上是一個請求序列號Sequence),由服務端提供
boolean idempotent_withdraw(ticket_id, account_id, amount);//實現了冪等性的取款操作,使用服務端提供的ticket_id,實際上是一個請求序列號Sequence


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方法不具備冪等性。

比如:POST http://www.forum.com/articles的語義是在http://www.forum.com/articles下建立一篇帖子,HTTP響應中應包含帖子的建立狀態以及帖子的URI。兩次相同的POST請求會在伺服器端建立兩份資源,它們具有不同的URI;所以,POST方法不具備冪等性。

PUT所對應的URI是要建立或更新的資源本身,PUT方法具有冪等性。

比如: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中。這種模式可以應用於很多場合,比如:論壇網站中防止意外的重複發帖。

理解:非冪等操作的副作用問題處理一般採用伺服器端生成一個請求序列號Sequence,客戶端使用序列號Sequence作為冪等操作的憑據去實現方法內冪等操作。圖1和圖2描述的就是這麼一個過程。另外對於HTTP GET、DELETE、PUT、POST四種主要方法的語義和冪等性做了相應的說明和糾正。

實現冪等設計的方法

查詢操作

查詢一次和查詢多次,在資料不變的情況下,查詢結果是一樣的。select是天然的冪等操作。

刪除操作

刪除操作也是冪等的,刪除一次和多次刪除都是把資料刪除。(注意可能返回結果不一樣,刪除的資料不存在,返回0,刪除的資料多條,返回結果多個)。

唯一索引,防止新增髒資料

比如:支付寶的資金賬戶,支付寶也有使用者賬戶,每個使用者只能有一個資金賬戶,怎麼防止給使用者建立資金賬戶多個,那麼給資金賬戶表中的使用者ID加唯一索引,所以一個使用者新增成功一個資金賬戶記錄。

要點:唯一索引或唯一組合索引來防止新增資料存在髒資料(當表存在唯一索引,併發時新增報錯時,再查詢一次就可以了,資料應該已經存在了,返回結果即可)。

Token機制,防止頁面重複提交

業務要求:
頁面的資料只能被點選提交一次
發生原因:
由於重複點選或者網路重發,或者nginx重發等情況會導致資料被重複提交
解決辦法:
叢集環境:採用token加redis(redis單執行緒的,處理需要排隊)
單JVM環境:採用token加redis或token加jvm記憶體
處理流程:
1. 資料提交前要向服務的申請token,token放到redis或jvm記憶體,token有效時間
2. 提交後後臺校驗token,同時刪除token,生成新的token返回
Token特點:

要申請,一次有效性,可以限流。

注意:redis要用刪除操作來判斷token,刪除成功代表token校驗通過,如果用select delete來校驗token,存在併發問題,不建議使用.

悲觀鎖

獲取資料的時候加鎖獲取
select * from table_xxx where id=’xxx’ for update;

注意:id欄位一定是主鍵或者唯一索引,不然是鎖表,會死人的,悲觀鎖使用時一般伴隨事務一起使用,資料鎖定時間可能會很長,根據實際情況選用。

樂觀鎖

樂觀鎖只是在更新資料那一刻鎖表,其他時間不鎖表,所以相對於悲觀鎖,效率更高。

樂觀鎖的實現方式多種多樣可以通過version或者其他狀態條件:
1. 通過版本號實現

update table_xxx set name=#name#,version=version 1 where version=#version#

2. 通過條件限制

update table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0

要求:quality-#subQuality# >= ,這個情景適合不用版本號,只更新是做資料安全校驗,適合庫存模型,扣份額和回滾份額,效能更高。

注意:樂觀鎖的更新操作,最好用主鍵或者唯一索引來更新,這樣是行鎖,否則更新時會鎖表,上面兩個sql改成下面的兩個更好

update table_xxx set name=#name#,version=version 1 where id=#id# and version=#version#
update table_xxx set avai_amount=avai_amount-#subAmount# where id=#id# a

分散式鎖

還是拿插入資料的例子,如果是分佈是系統,構建全域性唯一索引比較困難,例如唯一性的欄位沒法確定,這時候可以引入分散式鎖,通過第三方的系統(redis或zookeeper),在業務系統插入資料或者更新資料,獲取分散式鎖,然後做操作,之後釋放鎖,這樣其實是把多執行緒併發的鎖的思路,引入多多個系統,也就是分散式系統中得解決思路。

要點:某個長流程處理過程要求不能併發執行,可以在流程執行之前根據某個標誌(使用者ID 字尾等)獲取分散式鎖,其他流程執行時獲取鎖就會失敗,也就是同一時間該流程只能有一個能執行成功,執行完成後,釋放分散式鎖(分散式鎖要第三方系統提供)。

select insert

併發不高的後臺系統,或者一些任務JOB,為了支援冪等,支援重複執行,簡單的處理方法是,先查詢下一些關鍵資料,判斷是否已經執行過,在進行業務處理,就可以了
注意:核心高併發流程不要用這種方法。

狀態機冪等

在設計單據相關的業務,或者是任務相關的業務,肯定會涉及到狀態機(狀態變更圖),就是業務單據上面有個狀態,狀態在不同的情況下會發生變更,一般情況下存在有限狀態機,這時候,如果狀態機已經處於下一個狀態,這時候來了一個上一個狀態的變更,理論上是不能夠變更的,這樣的話,保證了有限狀態機的冪等。

注意:訂單等單據類業務,存在很長的狀態流轉,一定要深刻理解狀態機,對業務系統設計能力提高有很大幫助。

對外提供介面的api如何保證冪等

如銀聯提供的付款介面:需要接入商戶提交付款請求時附帶:source來源,seq序列號source seq在資料庫裡面做唯一索引,防止多次付款,(併發時,只能處理一個請求)

重點:對外提供介面為了支援冪等呼叫,介面有兩個欄位必須傳,一個是來源source,一個是來源方序列號seq,這個兩個欄位在提供方系統裡面做聯合唯一索引,這樣當第三方呼叫時,先在本方系統裡面查詢一下,是否已經處理過,返回相應處理結果;沒有處理過,進行相應處理,返回結果。注意,為了冪等友好,一定要先查詢一下,是否處理過該筆業務,不查詢直接插入業務系統,會報錯,但實際已經處理了。

SOA重複訊息冪等設計

重複訊息是SOA服務實現中非常常見的問題,你永遠不要指望呼叫方每次請求訊息不一樣,對於讀操作,重複訊息可能無害,可對於寫操作很可能就是災難。可以通過冪等(Idempotent)模式處理重複的訊息,基本處理思路是:

1、呼叫者給訊息一個唯一請求ID標識。ID標識一個工作單元,這個工作單元只應執行一次,工作單元ID可以是Schema的一部分,也可以是一個定製的SOAP Header,服務的Contract 可以說明這個唯一請求ID標識是必須的;

2、接收者在執行一個工作單元必須先檢驗該工作單元是否已經執行過。檢查是否執行的邏輯通常是根據唯一請求ID ,在服務端查詢請求是否有記錄,是否有對應的響應資訊,如果有,直接把響應資訊查詢後返回;如果沒有,那麼就當做新請求去處理。

參考資料

百度百科-冪等:http://baike.baidu.com/link?url=LL3VHg4dEBkgOF9yBbOs_TSYPNIHypc9-6uC3JBvelFAW8_gdkm5U1Z6A0HY9xi15Yc0S6rG5_aYhOX6fPN0horHULDvNnsBr5Q6DCPaWl7

理解HTTP冪等性:http://www.cnblogs.com/weidagang2046/archive/2011/06/04/2063696.html


分散式系統介面冪等性:http://blog.brucefeng.info/post/api-idempotent或:http://www.360doc.com/content/16/0428/09/21340737_554437264.shtml

高併發的核心技術-冪等的實現方案:http://825635381.iteye.com/blog/2276077