1. 程式人生 > >深入理解冪等性及Restful風格API的冪等性問題詳解

深入理解冪等性及Restful風格API的冪等性問題詳解

什麼是冪等性

HTTP/1.1中對冪等性的定義是:一次和多次請求某一個資源對於資源本身應該具有同樣的結果(網路超時等問題除外)。也就是說,其任意多次執行對資源本身所產生的影響均與一次執行的影響相同。
這裡需要關注幾個重點:
冪等不僅僅只是一次(或多次)請求對資源沒有副作用(比如查詢資料庫操作,沒有增刪改,因此沒有對資料庫有任何影響)。
冪等還包括第一次請求的時候對資源產生了副作用,但是以後的多次請求都不會再對資源產生副作用。
冪等關注的是以後的多次請求是否對資源產生的副作用,而不關注結果。
網路超時等問題,不是冪等的討論範圍。
冪等性是系統服務對外一種承諾(而不是實現),承諾只要呼叫介面成功,外部多次呼叫對系統的影響是一致的。宣告為冪等的服務會認為外部呼叫失敗是常態,並且失敗之後必然會有重試。

什麼情況下需要冪等性

業務開發中,經常會遇到重複提交的情況,無論是由於網路問題無法收到請求結果而重新發起請求,或是前端的操作抖動而造成重複提交情況。 在交易系統,支付系統這種重複提交造成的問題有尤其明顯,比如:

使用者在APP上連續點選了多次提交訂單,後臺應該只產生一個訂單;

向支付寶發起支付請求,由於網路問題或系統BUG重發,支付寶應該只扣一次錢。 很顯然,宣告冪等的服務認為,外部呼叫者會存在多次呼叫的情況,為了防止外部多次呼叫對系統資料狀態的發生多次改變,將服務設計成冪等。

冪等與防重的辨別

上面例子中小明遇到的問題,只是重複提交的情況,和服務冪等的初衷是不同的。重複提交是在第一次請求已經成功的情況下,人為的進行多次操作,導致不滿足冪等要求的服務多次改變狀態。而冪等更多使用的情況是第一次請求不知道結果(比如超時)或者失敗的異常情況下,發起多次請求,目的是多次確認第一次請求成功,卻不會因多次請求而出現多次的狀態變化。

什麼情況下需要保證冪等性

以SQL為例,有下面三種場景,只有第三種場景需要開發人員使用其他策略保證冪等性:

SELECT col1 FROM tab1 WHER col2=2,無論執行多少次都不會改變狀態,是天然的冪等。

UPDATE tab1 SET col1=1 WHERE col2=2,無論執行成功多少次狀態都是一致的,因此也是冪等操作。

UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次執行的結果都會發生變化,這種不是冪等的。

為什麼要設計冪等性的服務

冪等可以使得客戶端邏輯處理變得簡單,但是卻以服務邏輯變得複雜為代價。滿足冪等服務的需要在邏輯中至少包含兩點:

首先去查詢上一次的執行狀態,如果沒有則認為是第一次請求

在服務改變狀態的業務邏輯前,保證防重複提交的邏輯

冪等的不足

冪等是為了簡化客戶端邏輯處理,卻增加了服務提供者的邏輯和成本,是否有必要,需要根據具體場景具體分析,因此除了業務上的特殊要求外,儘量不提供冪等的介面。

增加了額外控制冪等的業務邏輯,複雜化了業務功能;

把並行執行的功能改為序列執行,降低了執行效率。

保證冪等的策略

冪等需要通過唯一的業務單號來保證。也就是說相同的業務單號,認為是同一筆業務。使用這個唯一的業務單號來確保,後面多次的相同的業務單號的處理邏輯和執行效果是一致的。 下面以支付為例,在不考慮併發的情況下,實現冪等很簡單:①先查詢一下訂單是否已經支付過,②如果已經支付過,則返回支付成功;如果沒有支付,進行支付流程,修改訂單狀態為‘已支付’。

防止重複提交的策略

上述的保證冪等方案是分成兩步的,第②步依賴第①步的查詢結果,無法保證原子性的。在高併發下就會出現下面的情況:第二次請求在第一次請求第②步訂單狀態還沒有修改為‘已支付狀態’的情況下到來。既然得出了這個結論,餘下的問題也就變得簡單:把查詢和變更狀態操作加鎖,將並行操作改為序列操作。

樂觀鎖

如果只是更新已有的資料,沒有必要對業務進行加鎖,設計表結構時使用樂觀鎖,一般通過version來做樂觀鎖,這樣既能保證執行效率,又能保證冪等。例如: UPDATE tab1 SET col1=1,version=version+1 WHERE version=#version# 不過,樂觀鎖存在失效的情況,就是常說的ABA問題,不過如果version版本一直是自增的就不會出現ABA的情況。如果version的版本不是自增的, 我們可以加一個時間戳,保證時間戳欄位不會被更新,驗證時間戳的一致性即可。可以這樣想。假設我們有兩個執行緒,第一個執行緒取到了version,第二個執行緒也取到了version。然後第一個執行緒更新的同時對version進行增加操作,這樣第二個執行緒即使執行了update,也不會操作到,因為version已經改變了。

防重表

使用訂單號orderNo做為去重表的唯一索引,每次請求都根據訂單號向去重表中插入一條資料。第一次請求查詢訂單支付狀態,當然訂單沒有支付,進行支付操作,無論成功與否,執行完後更新訂單狀態為成功或失敗,刪除去重表中的資料。後續的訂單因為表中唯一索引而插入失敗,則返回操作失敗,直到第一次的請求完成(成功或失敗)。可以看出防重表作用是加鎖的功能。
通過唯一索引的方式,進行,當相同的資料,往唯一的索引表插入的時候,就會報錯。
例子:
建立一個測試表

CREATE TABLE `test03` (
`id` INT(11) ,
`uid` INT(11) DEFAULT NULL);

建立唯一索引

ALTER IGNORE TABLE test03 ADD UNIQUE INDEX  id_uid(id,uid);

再次插入資料,發現相同的資料不能插入

INSERT INTO test03(id,uid) VALUES (1,1),(1,2),(1,1),(1,2),(1,1);

這種方式其實和下方的redis方式形式上差不多,而且下方的方式更快。

分散式鎖

這裡使用的防重表可以使用分散式鎖代替,比如Redis。訂單發起支付請求,支付系統會去Redis快取中查詢是否存在該訂單號的Key,如果不存在,則向Redis增加Key為訂單號。查詢訂單支付已經支付,如果沒有則進行支付,支付完成後刪除該訂單號的Key。通過Redis做到了分散式鎖,只有這次訂單訂單支付請求完成,下次請求才能進來。相比去重表,將放併發做到了快取中,較為高效。思路相同,同一時間只能完成一次支付請求。

token令牌

這種方式分成兩個階段:申請token階段和支付階段。 第一階段,在進入到提交訂單頁面之前,需要訂單系統根據使用者資訊向支付系統發起一次申請token的請求,支付系統將token儲存到Redis快取中,為第二階段支付使用。 第二階段,訂單系統拿著申請到的token發起支付請求,支付系統會檢查Redis中是否存在該token,如果存在,表示第一次發起支付請求,刪除快取中token後開始支付邏輯處理;如果快取中不存在,表示非法請求。 實際上這裡的token是一個信物,支付系統根據token確認,你是你媽的孩子。不足是需要系統間互動兩次,流程較上述方法複雜。

支付緩衝區

把訂單的支付請求都快速地接下來,一個快速接單的緩衝管道。後續使用非同步任務處理管道中的資料,過濾掉重複的待支付訂單。優點是同步轉非同步,高吞吐。不足是不能及時地返回支付結果,需要後續監聽支付結果的非同步返回。

RestfulApi冪等性詳解

我們要注意冪等是面向資源的。這個 HTTP GET 方法可能會每次得到不同的返回內容,但並不影響資源。

GET方法

HTTP GET 方法,用於獲取資源,不管呼叫多少次介面,結果都不會改變,所以是冪等的。只是查詢資料,不會影響到資源的變化,因此我們認為它冪等。

POST方法

HTTP POST 方法是一個非冪等方法,因為呼叫多次,都將產生新的資源。因為它會對資源本身產生影響,每次呼叫都會有新的資源產生,因此不滿足冪等性。

PUT方法

因為它直接把實體部分的資料替換到伺服器的資源,我們多次呼叫它,只會產生一次影響,但是有相同結果的 HTTP 方法,所以滿足冪等性。

Delete方法

HTTP DELETE 方法用於刪除資源,會將資源刪除。呼叫一次和多次對資源產生影響是相同的,所以也滿足冪等性。
所以對於restful來說,只有post是不滿足的