背景
在編程領(lǐng)域赫段,冪等性是指對(duì)同一個(gè)系統(tǒng)舵抹,使用同樣的條件背蟆,一次請(qǐng)求和重復(fù)的多次請(qǐng)求對(duì)系統(tǒng)資源的影響是一致的鉴分。
在分布式系統(tǒng)里,client 調(diào)用 server 提供的服務(wù)带膀,由于網(wǎng)絡(luò)環(huán)境的復(fù)雜性志珍,調(diào)用可能有以下幾種情況:
server 收到 client 的請(qǐng)求,client 也收到 server 的響應(yīng)結(jié)果
client 發(fā)出了請(qǐng)求垛叨,但 server 未收到伦糯,可能是 server 重啟、網(wǎng)絡(luò)超時(shí)等原因
server 發(fā)出了響應(yīng)嗽元,但 client 未收到
對(duì)于后兩種情況敛纲,client 一般會(huì)進(jìn)行一次重試,這樣 server 可能會(huì)收到多次重復(fù)的請(qǐng)求剂癌。對(duì)于某些天然就冪等的服務(wù)來(lái)說(shuō)淤翔,比如對(duì)資源的讀操作,不管讀多少次佩谷,資源不會(huì)有變化旁壮;但對(duì)非冪等服務(wù),server 執(zhí)行一次和重復(fù)執(zhí)行多次谐檀,對(duì)資源的影響就不確定了抡谐。
例如銀行扣款服務(wù),用函數(shù)表示為?bool withdraw(account_id, amount)桐猬,client 發(fā)起一次調(diào)用?withdraw(1001, 10)?請(qǐng)求從帳戶 1001 中扣除 10 元麦撵,如果發(fā)生了上圖所示的第 2 種錯(cuò)誤,這時(shí)候 server 端在帳戶里已經(jīng)完成了扣款,但 client 并不知道厦坛,如果重試調(diào)用?withdraw(1001, 10)?五垮,server 端又會(huì)從 帳戶 1001 扣除 10 元,顯然這個(gè)非冪等的扣款服務(wù)并不是 client 想要的杜秸。
如果將 client 的一次扣款操作和后續(xù)的重試用一個(gè)額外的 id 來(lái)標(biāo)識(shí):bool withdraw(id, account_id, amount),server 針對(duì)一個(gè) id 的相同請(qǐng)求只執(zhí)行一次润绎,這樣就可以避免上述的問(wèn)題了撬碟。此時(shí)扣款服務(wù)也是冪等的了。
按照上面介紹的冪等的扣款服務(wù)的實(shí)現(xiàn)思路莉撇,抽象出一個(gè)通用的中間層呢蛤,非冪等的服務(wù)要改造成冪等的,只需要增加一個(gè)額外的 id 參數(shù)棍郎。服務(wù)實(shí)現(xiàn)里先根據(jù)此 id 去中間層查詢服務(wù)是否執(zhí)行過(guò)其障,根據(jù)查詢結(jié)果決定的是否繼續(xù)后續(xù)的業(yè)務(wù)流程。中間層相當(dāng)于一個(gè)特殊的分布式互斥鎖涂佃,根據(jù) id 查詢的過(guò)程相當(dāng)于對(duì)某把鎖嘗試加鎖的操作励翼。鎖被鎖住后永遠(yuǎn)不釋放(除非鎖過(guò)期了,這里為了敘述方便簡(jiǎn)單認(rèn)為永遠(yuǎn)不釋放)辜荠。鎖被一個(gè)進(jìn)程鎖住后其他進(jìn)程都無(wú)法再加鎖汽抚,這樣就保證了服務(wù)是冪等的了。
第一個(gè)對(duì)互斥鎖加鎖的進(jìn)程任務(wù)沒(méi)有執(zhí)行完就掛掉伯病,鎖又是不會(huì)釋放的造烁,其他進(jìn)程又無(wú)法重復(fù)加鎖,導(dǎo)致這個(gè)失敗的任務(wù)也不能被其他進(jìn)程重新執(zhí)行午笛。為了避免這種情況惭蟋,將加鎖的操作分成 2 步:
TryAcquire
嘗試獲取鎖,結(jié)果有兩種情況:
1.1 拿到了鎖(鎖轉(zhuǎn)到 TryAcquired 狀態(tài))药磺,這時(shí)候可以執(zhí)行正常的業(yè)務(wù)流程告组,執(zhí)行完了需要再調(diào)用第二步 Confirm 明確鎖已被鎖住(鎖轉(zhuǎn)到 Confirmed 狀態(tài)),這之后其他進(jìn)程都拿不到這把鎖与涡;
1.2 沒(méi)拿到鎖惹谐,可能是以下三種情況之一:
1.2.1 鎖處于 Confirmed 狀態(tài),這種情況不應(yīng)該繼續(xù)業(yè)務(wù)流程處理直接返回驼卖;
1.2.2 鎖處于 TryAcquired 狀態(tài)氨肌,但超時(shí)時(shí)間沒(méi)到,說(shuō)明這個(gè)時(shí)候有其他進(jìn)程拿到了鎖正在進(jìn)行相應(yīng)的業(yè)務(wù)流程酌畜,本進(jìn)程不應(yīng)該執(zhí)行相應(yīng)的業(yè)務(wù)流程直接返回怎囚;
1.2.3 鎖處于 TryAcquired 狀態(tài),但超時(shí)時(shí)間到了,說(shuō)明已有其他進(jìn)程拿到了鎖恳守,但很久沒(méi)有 Confirm 考婴,有可能是執(zhí)行過(guò)程中掛掉了,這時(shí)候本進(jìn)程應(yīng)該要執(zhí)行相應(yīng)的業(yè)務(wù)流程催烘,然后調(diào)用第二步 Confirm 沥阱。
Confirm
將鎖置成 Confirmed 狀態(tài),表示互斥鎖被永久鎖住伊群。
鎖的狀態(tài)轉(zhuǎn)換如下所示(expire 為 redis key 過(guò)期):
使用 Redis 實(shí)現(xiàn)考杉,key 為互斥鎖的標(biāo)識(shí),value 為鎖的狀態(tài):
0:初始狀態(tài)* -1:Confirmed 狀態(tài)
其他值:TryAcquired 狀態(tài)舰始,value 為業(yè)務(wù)執(zhí)行截止時(shí)間 deadline
server 在增加了保證冪等性的流程圖如下(交易表示既定的業(yè)務(wù)執(zhí)行流程):
流程圖里省略了 redis 錯(cuò)誤處理的分支崇棠,redis 錯(cuò)誤 TryAcquire 直接返回 true 。
TryAcqurie 和 Confirm 實(shí)現(xiàn)用偽碼描述如下:
id 由 client 根據(jù)具體的業(yè)務(wù)場(chǎng)景決定丸卷,可以本地生成或者是從第三方服務(wù)獲取枕稀,要求需要保證能唯一標(biāo)識(shí)某個(gè)業(yè)務(wù)下的一次交易。server 端將此 id 視為互斥鎖的唯一標(biāo)識(shí)谜嫉。
timeout 應(yīng)該比正常的交易時(shí)間大萎坷,否則會(huì)導(dǎo)致多個(gè)進(jìn)程都能拿到鎖不能保證冪等;但是又不能設(shè)得太大骄恶,否則會(huì)導(dǎo)致交易執(zhí)行失敗時(shí)要過(guò)很久才能重新執(zhí)行交易食铐。
TryAcquire 和 Confirm 都應(yīng)該保證原子性,Confirm 只有一個(gè)簡(jiǎn)單的 SET 操作僧鲁,這個(gè)沒(méi)有問(wèn)題虐呻。TryAcquire 實(shí)際上分成兩步:1.1 SETNX 和 1.2 GET&SET(不是 redis 是 GETSET 命令)。 上面的偽碼中 1.2 GET&SET 的 SET 換成了 INCRBY 并增加了一次返回值比較寞秃,相當(dāng)于使用了樂(lè)觀鎖斟叼,所以 GET&SET 的原子性是 OK 的。在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流裙春寿。交流學(xué)習(xí)裙號(hào):821169538朗涩,里面會(huì)分享一些資深架構(gòu)師錄制的視頻錄像
下面說(shuō)明下為什么 1.1 和 1.2 整個(gè)過(guò)程沒(méi)有保證原子性也是 OK 的:
最壞的情況下假設(shè)進(jìn)程 a 進(jìn)入 TryAcquire 執(zhí)行完了 1.1 然后被操作系統(tǒng)調(diào)度出去了,此時(shí)進(jìn)程 b 進(jìn)入 TryAcquire 執(zhí)行了整個(gè)流程拿到了鎖绑改,然后執(zhí)行了一次交易谢床。這時(shí)候進(jìn)程 a 重新被調(diào)度執(zhí)行,這個(gè)時(shí)候由于進(jìn)程 b 更新了 deadline 甚至執(zhí)行完了 Confirm厘线,進(jìn)程 a 會(huì)在 1.2.1 或 1.2.2 處退出并且不會(huì)執(zhí)行交易识腿,如果走到了 1.2.3 并且拿到了鎖說(shuō)明進(jìn)程 b 執(zhí)行交易時(shí)掛掉了,這時(shí)由進(jìn)程 a 重新執(zhí)行交易也是正確的邏輯造壮。
這個(gè)方案忽略了 redis 異常情況渡讼,這種情況下 TryAcquire 總是返回 true ,可能會(huì)使交易重復(fù)執(zhí)行不能保證冪等。也可以將 redis 異常返回給調(diào)用者成箫,由調(diào)用者根據(jù)業(yè)務(wù)場(chǎng)景來(lái)決定是否需要重新執(zhí)行交易展箱。
另外一種情況進(jìn)程通過(guò) TryAcquire 拿到鎖后執(zhí)行完了交易,但 Confirm 失敗(掛掉或者網(wǎng)絡(luò)問(wèn)題)蹬昌,這種情況在 dealine 到了后混驰,其他進(jìn)程仍然可以拿到鎖并執(zhí)行交易,這時(shí)候也不能保證冪等凳厢。
缺陷的本質(zhì)是這個(gè)輕量級(jí)的解決方案無(wú)法保證分布式事務(wù)的原子性账胧。