分布式系統(tǒng)的防重和冪等實(shí)現(xiàn)機(jī)制
隨著微服務(wù)與云原生技術(shù)的普及,分布式系統(tǒng)已成為現(xiàn)代軟件架構(gòu)的主流。然而,系統(tǒng)的分布式特性也帶來了新的挑戰(zhàn),尤其是在網(wǎng)絡(luò)延遲、請(qǐng)求重試、并發(fā)操作等復(fù)雜場(chǎng)景下,如何保證數(shù)據(jù)的一致性、操作的正確性以及服務(wù)的可用性,成為了系統(tǒng)設(shè)計(jì)的核心難點(diǎn)。本文將介紹分布式架構(gòu)下的2個(gè)關(guān)鍵穩(wěn)定性保障機(jī)制:請(qǐng)求防重和接口冪等性。
1、分布式請(qǐng)求防重策略與技術(shù)
在分布式環(huán)境中,由于客戶端重試、網(wǎng)絡(luò)抖動(dòng)、網(wǎng)關(guān)超時(shí)重發(fā)或消息隊(duì)列的重投機(jī)制,同一個(gè)請(qǐng)求可能會(huì)多次到達(dá)服務(wù)端。請(qǐng)求防重的核心目標(biāo)是識(shí)別并丟棄這些重復(fù)的請(qǐng)求,確保一個(gè)業(yè)務(wù)邏輯在面對(duì)完全相同的多次請(qǐng)求時(shí),僅被有效執(zhí)行一次。這不僅可以避免數(shù)據(jù)冗余和錯(cuò)誤,也是實(shí)現(xiàn)接口冪等性的重要前提。
1.1 唯一請(qǐng)求ID與防重表
這是一種基于數(shù)據(jù)庫或持久化存儲(chǔ)的強(qiáng)一致性防重方案,其核心思想是為每一次業(yè)務(wù)操作生成一個(gè)全局唯一的標(biāo)識(shí)符,并在處理前進(jìn)行校驗(yàn)。
在技術(shù)實(shí)現(xiàn)上要求調(diào)用方在發(fā)起請(qǐng)求時(shí),攜帶一個(gè)全局唯一的請(qǐng)求ID。服務(wù)端在接收到請(qǐng)求后,首先會(huì)嘗試將這個(gè)請(qǐng)求ID插入到一個(gè)專用的“防重表”中。這個(gè)表的關(guān)鍵在于為請(qǐng)求ID字段設(shè)置了唯一性約束。具體流程如下:
- 生成ID:客戶端或上游服務(wù)在發(fā)起寫操作請(qǐng)求前,生成一個(gè)全局唯一的request_id。
- 攜帶ID請(qǐng)求:客戶端將request_id連同業(yè)務(wù)參數(shù)一同發(fā)送至服務(wù)端。
- 原子化插入校驗(yàn):服務(wù)端在執(zhí)行核心業(yè)務(wù)邏輯之前,嘗試執(zhí)行INSERT INTO deduplication_table (request_id) VALUES (?)。
- 結(jié)果判斷與處理:a) 如果插入成功,說明這是一個(gè)新的請(qǐng)求。服務(wù)端繼續(xù)執(zhí)行后續(xù)的業(yè)務(wù)邏輯。b) 如果插入失敗并拋出唯一鍵沖突的異常,則證明該request_id已被處理過。服務(wù)端捕獲此異常,識(shí)別為重復(fù)請(qǐng)求,直接丟棄或返回先前處理的結(jié)果,不再執(zhí)行業(yè)務(wù)邏輯 。
圖片
該技術(shù)的優(yōu)勢(shì)是在于其極高的可靠性,能夠利用數(shù)據(jù)庫的ACID特性,從根本上保證數(shù)據(jù)操作的唯一性。缺點(diǎn)就是每次請(qǐng)求都需要進(jìn)行一次數(shù)據(jù)庫寫入操作,那么數(shù)據(jù)庫容易成為整個(gè)系統(tǒng)的性能瓶頸,尤其是在高并發(fā)場(chǎng)景下 。同時(shí),需要考慮防重表的清理機(jī)制,以避免其無限增長(zhǎng)。通常不會(huì)單獨(dú)設(shè)計(jì)防重表,而是在表設(shè)計(jì)的時(shí)候定義唯一鍵做防重。
1.2 分布式鎖
分布式鎖可以有效控制并發(fā),通過確保在分布式系統(tǒng)的多個(gè)節(jié)點(diǎn)中某個(gè)關(guān)鍵代碼塊在同一時(shí)間只能被一個(gè)線程執(zhí)行,從而間接實(shí)現(xiàn)請(qǐng)求防重。
實(shí)現(xiàn)原理也很簡(jiǎn)單,當(dāng)請(qǐng)求到達(dá)時(shí),服務(wù)端會(huì)根據(jù)請(qǐng)求中的業(yè)務(wù)關(guān)鍵信息組合成一個(gè)唯一的鎖key。然后,服務(wù)嘗試去獲取這個(gè)key對(duì)應(yīng)的分布式鎖。具體流程如下:
- 構(gòu)造鎖Key:服務(wù)端根據(jù)請(qǐng)求參數(shù)生成一個(gè)唯一的鎖標(biāo)識(shí),例如 lock:payment:order_id_123。
- 嘗試加鎖: 服務(wù)嘗試使用Redis的SETNX命令或Zookeeper的臨時(shí)節(jié)點(diǎn)等機(jī)制來獲取該鎖 。
- 結(jié)果判斷與處理:a) 如果成功獲取鎖,表明當(dāng)前沒有其他請(qǐng)求在處理此項(xiàng)業(yè)務(wù)。服務(wù)端開始執(zhí)行業(yè)務(wù)邏輯,并在完成后釋放鎖。b) 如果獲取鎖失敗,則說明已有另一個(gè)線程或進(jìn)程正在處理,當(dāng)前請(qǐng)求被視為重復(fù),應(yīng)立即返回或丟棄
分布式鎖的核心優(yōu)勢(shì)在于控制并發(fā),特別適合“處理中”狀態(tài)的防重,防止對(duì)同一資源的并發(fā)修改。技術(shù)實(shí)現(xiàn)上可以通過Redis,也可以通過Zookeeper的臨時(shí)節(jié)點(diǎn)完成。但其缺點(diǎn)是增加了系統(tǒng)的復(fù)雜性,需要處理鎖的超時(shí)、續(xù)期和安全釋放等問題,否則可能導(dǎo)致死鎖。鎖的粒度設(shè)計(jì)也至關(guān)重要,過大的粒度會(huì)降低系統(tǒng)吞吐量。
1.3 令牌機(jī)制
令牌機(jī)制主要用于防止客戶端因用戶誤操作(如快速點(diǎn)擊提交按鈕)或前端邏輯不完善導(dǎo)致的表單重復(fù)提交。在實(shí)現(xiàn)上分為兩個(gè)階段:獲取令牌和使用令牌。服務(wù)端為即將發(fā)生的寫操作預(yù)先生成一個(gè)一次性的、唯一的令牌。具體流程如下:
- 申請(qǐng)令牌:用戶訪問表單頁面時(shí),客戶端向服務(wù)端發(fā)起一個(gè)獲取令牌的請(qǐng)求。
- 頒發(fā)并存儲(chǔ)令牌:服務(wù)端生成一個(gè)唯一Token(如UUID),將其存儲(chǔ)在Redis等高速緩存中并設(shè)置較短的過期時(shí)間,然后將Token返回給客戶端 。客戶端通常將此Token存放在表單的隱藏域中。
- 提交時(shí)攜帶令牌:用戶提交表單時(shí),請(qǐng)求中必須攜帶此Token。
- 原子化驗(yàn)簽與銷毀:服務(wù)端接收請(qǐng)求后,會(huì)使用原子操作(如Redis+Lua腳本)來驗(yàn)證并刪除該Token。a) 如果Token存在且被成功刪除,則處理業(yè)務(wù)邏輯。b) 如果Token不存在(已被其他請(qǐng)求消耗或已過期),則判定為重復(fù)提交,拒絕處理 。
圖片
令牌機(jī)制能有效攔截來自前端的重復(fù)請(qǐng)求,實(shí)現(xiàn)簡(jiǎn)單。但它要求前端進(jìn)行配合,增加了前后端的交互次數(shù)。
1.4 狀態(tài)機(jī)約束
對(duì)于有明確生命周期和狀態(tài)流轉(zhuǎn)的業(yè)務(wù)對(duì)象(如訂單、工單),可以利用狀態(tài)機(jī)模型來實(shí)現(xiàn)防重。其核心思想是業(yè)務(wù)操作必須遵循預(yù)設(shè)的狀態(tài)流轉(zhuǎn)路徑。任何不符合當(dāng)前狀態(tài)的遷移動(dòng)作都被視為非法或重復(fù)操作。具體流程如下:
- 定義狀態(tài):為業(yè)務(wù)對(duì)象定義清晰的狀態(tài)集(如訂單狀態(tài):待支付、已支付、已發(fā)貨、已完成、已取消)。
- 接收操作請(qǐng)求:服務(wù)端接收到一個(gè)改變狀態(tài)的請(qǐng)求,例如“支付訂單”。
- 校驗(yàn)當(dāng)前狀態(tài):在執(zhí)行操作前,從數(shù)據(jù)庫或緩存中讀取訂單的當(dāng)前狀態(tài)。
- 判斷狀態(tài)遷移合法性:a) 如果當(dāng)前訂單狀態(tài)是“待支付”,則“支付”操作是合法的。服務(wù)端繼續(xù)執(zhí)行支付邏輯,并將狀態(tài)更新為“已支付”。為了防止并發(fā)下的狀態(tài)沖突,通常會(huì)結(jié)合樂觀鎖(版本號(hào))進(jìn)行更新。b) 如果當(dāng)前訂單狀態(tài)已經(jīng)是“已支付”,則再次收到的“支付”請(qǐng)求就是重復(fù)請(qǐng)求,應(yīng)直接拒絕 。
狀態(tài)機(jī)方案與業(yè)務(wù)邏輯緊密結(jié)合,邏輯清晰,實(shí)現(xiàn)優(yōu)雅且非??煽?。它不僅能防重,還能保證業(yè)務(wù)流程的正確性。其主要局限在于適用場(chǎng)景,僅限于那些可以被清晰地建模為有限狀態(tài)機(jī)的業(yè)務(wù)流程。
2、分布式接口冪等性實(shí)現(xiàn)策略與技術(shù)
冪等性是一個(gè)數(shù)學(xué)概念,是指一個(gè)操作無論執(zhí)行一次還是執(zhí)行多次,其產(chǎn)生的影響和結(jié)果都是相同的。在分布式系統(tǒng)中,由于網(wǎng)絡(luò)不可靠導(dǎo)致的重試是常態(tài),保證寫操作的冪等性對(duì)于避免數(shù)據(jù)錯(cuò)亂、資金損失等嚴(yán)重問題至關(guān)重要。
2.1 數(shù)據(jù)庫唯一約束
通過在數(shù)據(jù)庫表上為能夠唯一標(biāo)識(shí)業(yè)務(wù)的字段(或字段組合)建立唯一索引,來利用數(shù)據(jù)庫自身的機(jī)制阻止重復(fù)數(shù)據(jù)的插入。具體流程如下:
- 識(shí)別唯一業(yè)務(wù)鍵:在設(shè)計(jì)表結(jié)構(gòu)時(shí),確定一個(gè)能唯一標(biāo)識(shí)一筆交易或一個(gè)實(shí)體的字段。
- 創(chuàng)建唯一索引:為該字段創(chuàng)建UNIQUE INDEX。
- 執(zhí)行插入操作:當(dāng)服務(wù)需要?jiǎng)?chuàng)建一個(gè)新記錄時(shí),直接執(zhí)行INSERT。
- 處理執(zhí)行結(jié)果:a) 第一次請(qǐng)求,INSERT成功,數(shù)據(jù)被創(chuàng)建。b) 后續(xù)的重試請(qǐng)求,由于transaction_id已存在,INSERT會(huì)失敗,數(shù)據(jù)庫返回唯一鍵沖突錯(cuò)誤。應(yīng)用層捕獲此錯(cuò)誤后,即可判定這是一個(gè)重復(fù)的創(chuàng)建操作,從而保證了“創(chuàng)建”這一行為的冪等性 。
圖片
該方案簡(jiǎn)單、高效,且保證了最終一致性,但它主要適用于INSERT場(chǎng)景。對(duì)于UPDATE操作,需要借助其他策略。同時(shí),在高并發(fā)寫入場(chǎng)景下,唯一索引的沖突檢查可能會(huì)對(duì)數(shù)據(jù)庫性能造成一定壓力。
2.2 樂觀鎖/版本號(hào)機(jī)制
樂觀鎖是實(shí)現(xiàn)UPDATE操作冪等性的經(jīng)典方案。它假設(shè)在操作期間數(shù)據(jù)不會(huì)被其他事務(wù)所修改,直到提交時(shí)才進(jìn)行檢查。該策略通常通過在數(shù)據(jù)表中增加一個(gè)version(版本號(hào))或timestamp(時(shí)間戳)字段來實(shí)現(xiàn)。具體流程如下:
- 讀取數(shù)據(jù)與版本號(hào):SELECT data, version FROM my_table WHERE id = ?;。
- 執(zhí)行業(yè)務(wù)計(jì)算:在內(nèi)存中根據(jù)讀取的data進(jìn)行業(yè)務(wù)邏輯計(jì)算。
- 帶版本號(hào)更新:提交更新時(shí),在UPDATE語句的WHERE子句中加入對(duì)版本號(hào)的檢查:UPDATE my_table SET data = ‘new_data’, version = version + 1 WHERE id = ? AND version = ‘old_version’;。
- 檢查更新結(jié)果:a) 如果UPDATE影響的行數(shù)為1,說明在操作期間沒有其他請(qǐng)求修改過數(shù)據(jù),更新成功。b) 如果影響的行數(shù)為0,說明version已被其他請(qǐng)求改變,當(dāng)前操作基于的是舊數(shù)據(jù)。此時(shí),可以判定為冪等沖突(或并發(fā)沖突),應(yīng)放棄本次修改或重新讀取數(shù)據(jù)重試 。
圖片
樂觀鎖避免了悲觀鎖長(zhǎng)時(shí)間的資源鎖定,因此在高并發(fā)讀多寫少的場(chǎng)景下有很好的性能表現(xiàn)。它能有效解決UPDATE操作的冪等問題。缺點(diǎn)是增加了業(yè)務(wù)邏輯的復(fù)雜性,應(yīng)用層需要處理更新失敗后的重試邏輯。
2.3 全局冪等令牌
該策略將冪等校驗(yàn)邏輯與業(yè)務(wù)邏輯解耦,適用于INSERT、UPDATE和DELETE等多種操作。這里的令牌代表的是一個(gè)完整的業(yè)務(wù)操作,而不僅僅是一次HTTP提交。
- 生成令牌:調(diào)用方(客戶端或其他服務(wù))在發(fā)起一個(gè)需要保證冪等的業(yè)務(wù)操作前,需生成一個(gè)全局唯一的冪等令牌idempotency_key。
- 攜帶令牌請(qǐng)求:將idempotency_key通過請(qǐng)求頭或請(qǐng)求體傳遞給服務(wù)端。
- 原子化檢查與鎖定:服務(wù)端收到請(qǐng)求后,以idempotency_key為鍵,使用原子命令(如Redis的SET key value NX EX)嘗試在共享緩存中創(chuàng)建一個(gè)占位記錄。
- 處理流程:a) 首次請(qǐng)求:SET成功,表明這是此idempotency_key的第一次請(qǐng)求。服務(wù)開始執(zhí)行業(yè)務(wù)邏輯。執(zhí)行完畢后,可以將執(zhí)行結(jié)果存入緩存,與idempotency_key關(guān)聯(lián),并為該key設(shè)置一個(gè)更長(zhǎng)的過期時(shí)間 。b) 重試請(qǐng)求:SET失敗,表明該idempotency_key已存在。服務(wù)端可以直接從緩存中查詢并返回上一次的執(zhí)行結(jié)果,從而保證了冪等性 。
圖片
該方案非常靈活,不侵入核心業(yè)務(wù)表的結(jié)構(gòu),且能夠通過返回緩存結(jié)果來優(yōu)化重試請(qǐng)求的體驗(yàn)。它依賴于一個(gè)高可用的分布式緩存系統(tǒng)(如Redis)。令牌的生成、傳遞和存儲(chǔ)管理也引入了額外的系統(tǒng)復(fù)雜性
2.4 狀態(tài)機(jī)流轉(zhuǎn)控制
通過嚴(yán)格控制業(yè)務(wù)實(shí)體的狀態(tài)流轉(zhuǎn),確保操作只能在特定狀態(tài)下執(zhí)行,從而實(shí)現(xiàn)冪等。這與防重部分的狀態(tài)機(jī)原理一致,但在冪等性語境下,更強(qiáng)調(diào)操作對(duì)最終狀態(tài)的影響是唯一的。一個(gè)操作是否執(zhí)行,取決于業(yè)務(wù)實(shí)體當(dāng)前的狀態(tài)是否允許該操作發(fā)生。
- 加載實(shí)體與狀態(tài):接收到操作請(qǐng)求后,加載業(yè)務(wù)實(shí)體及其當(dāng)前狀態(tài)。
- 驗(yàn)證狀態(tài)轉(zhuǎn)移:根據(jù)預(yù)定義的狀態(tài)機(jī)模型,判斷當(dāng)前狀態(tài)是否允許執(zhí)行請(qǐng)求的操作。例如,只有在“待發(fā)貨”狀態(tài)下,才能執(zhí)行“發(fā)貨”操作。
- 執(zhí)行與狀態(tài)更新:a) 如果狀態(tài)轉(zhuǎn)移合法,則執(zhí)行業(yè)務(wù)操作,并原子性地(通常結(jié)合樂觀鎖)將實(shí)體更新到下一個(gè)狀態(tài)。b) 如果狀態(tài)轉(zhuǎn)移非法,則直接拒絕操作,返回錯(cuò)誤信息。因?yàn)闊o論多少次非法的操作請(qǐng)求,都不會(huì)改變實(shí)體的當(dāng)前狀態(tài),從而保證了冪等性 。
狀態(tài)機(jī)是與業(yè)務(wù)領(lǐng)域模型高度耦合的冪等實(shí)現(xiàn)方式,邏輯嚴(yán)謹(jǐn),一旦模型建立,冪等性就有了天然的保障。其缺點(diǎn)是適用范圍有限,主要用于具有明確、有限狀態(tài)的業(yè)務(wù)流程。對(duì)于無狀態(tài)的通用接口,此方法不適用。
分布式系統(tǒng)的健壯性需要將防重和冪等作為架構(gòu)設(shè)計(jì)的核心考量。請(qǐng)求防重是數(shù)據(jù)正確性的保障,通過唯一ID、分布式鎖、令牌和狀態(tài)機(jī)等手段,可以有效過濾掉意外的重復(fù)請(qǐng)求;接口冪等性保證了在不可靠網(wǎng)絡(luò)環(huán)境下,利用數(shù)據(jù)庫約束、樂觀鎖、全局令牌和狀態(tài)機(jī),可以確保重試操作不會(huì)產(chǎn)生非預(yù)期的副作用。



































