在編程領域,冪等性是指對同一個系統,使用一樣的條件,一次請求和重複的屢次請求對系統資源的影響是一致的。redis
在分佈式系統裏,服務一般經過 RPC 或 HTTP 或其餘形式對外提供。無論怎樣,client 調用 server 服務都是將調用數據按特定協議封裝好,而後經過網絡發送給 server,server 將須要返回的數據一樣按特定協議封裝而後經過網絡發送給 client。因爲網絡環境的複雜性,client 在發起調用時,數據可能在到 server 鏈路中丟失,也可能在從 server 返回的鏈路中丟失。無論哪一種狀況,對 client 來講都是調用失敗。一般 client 會發起一次重試,若是是後者,那 server 就會收到屢次徹底同樣的請求。若是 server 的服務不是冪等的話,就可能出現問題。編程
典型的例子是銀行扣款服務,用函數表示爲 bool withdraw(account_id, amount)
,client 發起一次調用 withdraw(1001, 10)
請求從賬戶 1001 中扣除 10 元,若是發生了上圖所示的第 2 種錯誤,這時候 server 端在賬戶裏已經完成了扣款,但 client 並不知道,若是重試調用 withdraw(1001, 10)
,server 端又會從 賬戶 1001 扣除 10 元,顯然這並非 client 想要的。若是將 client 的此次扣款操做和後續的重試用一個統一的 id 來標識,server 針對一個 id 的相同請求只執行一次,這樣就能夠避免上述的問題了。也就是說扣款服務是冪等的。網絡
爲了方便 server 將服務實現成冪等的,本文介紹了一種使用 redis 實現的分佈式中間件方案。從上面的例子中能夠看出,實現冪等服務 client 除了服務正常的參數外還須要傳一個額外的 id 。這個 id 一般由 client 根據具體的業務場景決定,要求至少能保證一段時間內不會重複。架構
實際上至關於實現一個特殊的分佈式互斥鎖,一把鎖只能被一個進程鎖一次,永遠不釋放(除非鎖過時了,默認過時時間1天,這裏爲了敘述方便簡單認爲永遠不釋放)。分佈式
一把互斥鎖被一個進程加鎖後其餘進程都拿不到鎖,經過這種方式實現冪等性。函數
第一個拿到互斥鎖的進程任務沒有執行完就掛掉,鎖又是不會釋放的,其餘進程也拿不到鎖,致使這個失敗的任務也不能被其餘進程從新執行。 爲了不這種狀況,將加鎖的操做分紅 2 步:學習
鎖的狀態轉換以下所示(expire 爲 redis key 過時):在此我向你們推薦一個架構學習交流羣。交流學習羣號:821169538 ui
使用 Redis 實現,key 爲互斥鎖的標識,value 爲鎖的狀態:spa
server 在增長了保證冪等性的流程圖以下(交易表示既定的業務執行流程):操作系統
省略了 redis 錯誤處理的分支,redis 錯誤 TryAcquire 直接返回 true 。
TryAcqurie 和 Comfirm 實現用僞碼描述以下:
// return value: // true 能夠繼續業務流程,業務流程處理完後須要調用 Confirm // false 不能繼續業務流程 TryAcquire(id, timeout) { reply = SET id (now+timeout) EX 86400 NX // 1.1 if reply == 1 { return true } // 1.2 reply = GET id // 1.2.1 if reply == Confirmed { return false } // 1.2.2 if now < reply { return false } // 1.2.3 delta = now + timeout - reply new_reply = INCRBY id delta if new_reply == reply + delta { return true } else { DECRBY id delta } return false } Comfirm(id) { SET id -1 XX }
timeout 應該比正常的交易時間大,不然會致使多個進程都能拿到鎖不能保證冪等。可是又不能設得太大,不然會致使交易執行失敗時要過好久才能從新執行交易。
TryAcquire 和 Confirm 都應該保證原子性,Confirm 只有一個簡單的 SET 操做,這個沒有問題。TryAcquire 實際上分紅兩步:1.1 SETNX 和 1.2 GET&SET(不是 redis 是 GETSET 命令)。 上面的僞碼中 1.2 GET&SET 的 SET 換成了 INCRBY 並增長了一次返回值比較,至關於樂觀鎖的實現,因此 GET&SET 的原子性是 OK 的。
下面說明下爲何 1.1 和 1.2 整個過程沒有保證原子性也是 OK 的:
最壞的狀況下假設進程 a 進入 TryAcquire 執行完了 1.1 而後被操做系統調度出去了,此時進程 b 進入 TryAcquire 執行了整個流程拿到了鎖,而後執行了一次交易。這時候進程 a 從新被調度執行,這個時候因爲進程 b 更新了 deadline 甚至執行完了 Confirm,進程 a 會在 1.2.1 或 1.2.2 處退出而且不會執行交易,若是走到了 1.2.3 而且拿到了鎖說明進程 b 執行交易時掛掉了,這時由進程 a 從新執行交易也是正確的邏輯。
這個方案忽略了 redis 異常狀況,這種狀況下 TryAcquire 老是返回 true ,可能會使交易重複執行不能保證冪等。也能夠將 redis 異常返回給調用者,由調用者根據業務場景來決定是否須要從新執行交易。
另一種狀況進程經過 TryAcquire 拿到鎖後執行完了交易,但 Confirm 失敗(掛掉或者網絡問題),這種狀況在 dealine 到了後,其餘進程仍然能夠拿到鎖並執行交易,這時候也不能保證冪等。