背景redis
在編程領域,冪等性是指對同一個系統,使用一樣的條件,一次請求和重複的屢次請求對系統資源的影響是一致的。編程
在分佈式系統裏,client 調用 server 提供的服務,因爲網絡環境的複雜性,調用可能有如下幾種狀況:網絡
server 收到 client 的請求,client 也收到 server 的響應結果架構
client 發出了請求,但 server 未收到,多是 server 重啓、網絡超時等緣由分佈式
server 發出了響應,但 client 未收到ide
對於後兩種狀況,client 通常會進行一次重試,這樣 server 可能會收到屢次重複的請求。對於某些自然就冪等的服務來講,好比對資源的讀操做,無論讀多少次,資源不會有變化;但對非冪等服務,server 執行一次和重複執行屢次,對資源的影響就不肯定了。函數
例如銀行扣款服務,用函數表示爲 bool withdraw(account_id, amount),client 發起一次調用 withdraw(1001, 10) 請求從賬戶 1001 中扣除 10 元,若是發生了上圖所示的第 2 種錯誤,這時候 server 端在賬戶裏已經完成了扣款,但 client 並不知道,若是重試調用 withdraw(1001, 10) ,server 端又會從 賬戶 1001 扣除 10 元,顯然這個非冪等的扣款服務並非 client 想要的。post
若是將 client 的一次扣款操做和後續的重試用一個額外的 id 來標識:bool withdraw(id, account_id, amount),server 針對一個 id 的相同請求只執行一次,這樣就能夠避免上述的問題了。此時扣款服務也是冪等的了。學習
實現方案ui
按照上面介紹的冪等的扣款服務的實現思路,抽象出一個通用的中間層,非冪等的服務要改形成冪等的,只須要增長一個額外的 id 參數。服務實現裏先根據此 id 去中間層查詢服務是否執行過,根據查詢結果決定的是否繼續後續的業務流程。中間層至關於一個特殊的分佈式互斥鎖,根據 id 查詢的過程至關於對某把鎖嘗試加鎖的操做。鎖被鎖住後永遠不釋放(除非鎖過時了,這裏爲了敘述方便簡單認爲永遠不釋放)。鎖被一個進程鎖住後其餘進程都沒法再加鎖,這樣就保證了服務是冪等的了。
第一個對互斥鎖加鎖的進程任務沒有執行完就掛掉,鎖又是不會釋放的,其餘進程又沒法重複加鎖,致使這個失敗的任務也不能被其餘進程從新執行。爲了不這種狀況,將加鎖的操做分紅 2 步:
TryAcquire
嘗試獲取鎖,結果有兩種狀況:
1.1 拿到了鎖(鎖轉到 TryAcquired 狀態),這時候能夠執行正常的業務流程,執行完了須要再調用第二步 Confirm 明確鎖已被鎖住(鎖轉到 Confirmed 狀態),這以後其餘進程都拿不到這把鎖;
1.2 沒拿到鎖,多是如下三種狀況之一:
1.2.1 鎖處於 Confirmed 狀態,這種狀況不該該繼續業務流程處理直接返回;
1.2.2 鎖處於 TryAcquired 狀態,但超時時間沒到,說明這個時候有其餘進程拿到了鎖正在進行相應的業務流程,本進程不該該執行相應的業務流程直接返回;
1.2.3 鎖處於 TryAcquired 狀態,但超時時間到了,說明已有其餘進程拿到了鎖,但好久沒有 Confirm ,有多是執行過程當中掛掉了,這時候本進程應該要執行相應的業務流程,而後調用第二步 Confirm 。
Confirm
將鎖置成 Confirmed 狀態,表示互斥鎖被永久鎖住。
鎖的狀態轉換以下所示(expire 爲 redis key 過時):
使用 Redis 實現,key 爲互斥鎖的標識,value 爲鎖的狀態:
0:初始狀態* -1:Confirmed 狀態
其餘值:TryAcquired 狀態,value 爲業務執行截止時間 deadline
server 在增長了保證冪等性的流程圖以下(交易表示既定的業務執行流程):
流程圖裏省略了 redis 錯誤處理的分支,redis 錯誤 TryAcquire 直接返回 true 。
TryAcqurie 和 Confirm 實現用僞碼描述以下:
id 由 client 根據具體的業務場景決定,能夠本地生成或者是從第三方服務獲取,要求須要保證能惟一標識某個業務下的一次交易。server 端將此 id 視爲互斥鎖的惟一標識。
timeout 應該比正常的交易時間大,不然會致使多個進程都能拿到鎖不能保證冪等;可是又不能設得太大,不然會致使交易執行失敗時要過好久才能從新執行交易。
TryAcquire 和 Confirm 都應該保證原子性,Confirm 只有一個簡單的 SET 操做,這個沒有問題。TryAcquire 實際上分紅兩步:1.1 SETNX 和 1.2 GET&SET(不是 redis 是 GETSET 命令)。 上面的僞碼中 1.2 GET&SET 的 SET 換成了 INCRBY 並增長了一次返回值比較,至關於使用了樂觀鎖,因此 GET&SET 的原子性是 OK 的。在此我向你們推薦一個架構學習交流裙。交流學習裙號:821169538,裏面會分享一些資深架構師錄製的視頻錄像
下面說明下爲何 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 到了後,其餘進程仍然能夠拿到鎖並執行交易,這時候也不能保證冪等。
缺陷的本質是這個輕量級的解決方案沒法保證分佈式事務的原子性。