冪等性:構(gòu)建穩(wěn)健分布式系統(tǒng)的關(guān)鍵
為什么冪等性是關(guān)鍵?
在現(xiàn)代分布式系統(tǒng)中,可用性是關(guān)鍵因素,這意味著你需要構(gòu)建重試機制和處理失敗及恢復(fù)的方法。這也意味著你可能會在系統(tǒng)中重復(fù)處理相同的操作,但如果你的應(yīng)用程序不了解這一點,并將其視為一個全新的請求,它將產(chǎn)生不可預(yù)期的結(jié)果。如果是處理支付或管理電子商務(wù)訂單的應(yīng)用程序,這會導(dǎo)致巨大的財務(wù)損失和不可挽回的損害。
那么,我們該怎么做才能保證多次執(zhí)行操作的結(jié)果與僅執(zhí)行一次的結(jié)果相同呢?
讓系統(tǒng)具備冪等性?。。?/p>
什么是冪等性?
冪等性是指一個系統(tǒng)或過程在多次執(zhí)行相同操作時能夠產(chǎn)生相同結(jié)果的能力。冪等性保證了多次執(zhí)行相同操作不會引入意外的副作用,從而防止了意外的重復(fù)處理或不期望的更改。例如,在重復(fù)檢查中,冪等系統(tǒng)確保重復(fù)請求不會導(dǎo)致重復(fù)處理,因此如果向系統(tǒng)發(fā)出重復(fù)請求,冪等系統(tǒng)要么忽略它,要么返回首次處理的狀態(tài)。
等一下,我有點困惑?。?!重復(fù)檢查和冪等操作是一樣的嗎?
重復(fù)檢查 vs 冪等性
重復(fù)檢查旨在防止不必要的狀態(tài)更改,確保我們不會多次處理同一事件,而冪等性則允許我們再次處理相同的事件,但結(jié)果將是相同的。簡單來說,通過冪等性,我們可以在至少一次的消息傳遞系統(tǒng)中處理重復(fù)事件,確保多次處理相同事件仍然產(chǎn)生相同的效果。換句話說,冪等性是處理重復(fù)事件的最佳方式。
這聽起來不錯。但我們該如何實現(xiàn)它,可能會遇到什么挑戰(zhàn)呢?
冪等性實現(xiàn)策略
為了構(gòu)建冪等性,最重要的任務(wù)是為每個請求找到或創(chuàng)建一個冪等鍵。我制定了一個簡單的算法策略,并添加了一些標(biāo)準(zhǔn)鍵來構(gòu)建穩(wěn)健的冪等性。
策略:
- 為每個請求找到一個唯一的關(guān)聯(lián)標(biāo)識符,我們可以依賴它并存儲在數(shù)據(jù)存儲中,以便檢查每個傳入請求。
- 如果沒有唯一標(biāo)識符,可以通過對有效負(fù)載進(jìn)行哈希處理來創(chuàng)建校驗和,并將其用于冪等性。
- 如果有效負(fù)載包含UUID或時間戳(如創(chuàng)建時間戳),可能在每次重試時會改變,盡可能忽略這些字段并創(chuàng)建校驗和。如果無法忽略,請確保使用硬編碼值。
- 對于POST API,在請求頭中添加x-idempotency-key,并要求消費者提供一個唯一標(biāo)識符和一個可選的過期時間。
- 在服務(wù)網(wǎng)格中,服務(wù)協(xié)作執(zhí)行任務(wù)時,使用狀態(tài)變化模型和重復(fù)檢查來確保系統(tǒng)的冪等性。
- 定義冪等鍵的有效期,例如你的數(shù)據(jù)存儲將存儲該鍵的時間長度。
- 可以使用如下示例中展示的通用模型來創(chuàng)建復(fù)合冪等鍵:
public class IdempotentKey {
private String key; // 鍵
private long ttl; // 生存時間
private String result; // 響應(yīng)
}
冪等鍵
可用于構(gòu)建冪等鍵的標(biāo)準(zhǔn)鍵:
- UUID v4: 使用標(biāo)準(zhǔn)的java.util.UUID創(chuàng)建唯一標(biāo)識符。
- 有效負(fù)載哈希: 創(chuàng)建請求有效負(fù)載的哈希(摘要)作為冪等鍵,保證相同有效負(fù)載的請求生成相同的鍵。?鍵元素: 在冪等鍵中包含用戶ID、交易詳情和時間戳等唯一參數(shù),創(chuàng)建精確識別的復(fù)合鍵。
- 令牌: 發(fā)放并要求客戶端在請求中包含令牌,作為冪等鍵和安全措施。
- 時間戳: 使用時間戳或組合時間戳作為時間基冪等鍵,確保操作在定義的時間窗口內(nèi)是冪等的。
冪等模型
冪等性如何實現(xiàn),以及如何處理不同場景?下面是我開發(fā)的冪等性的shell級實現(xiàn)。
- 客戶端將有效負(fù)載“p12345”發(fā)送到接收服務(wù)進(jìn)行處理??蛻舳丝梢赃x擇在請求頭中發(fā)送x-idempotency-key,作為處理的冪等鍵。如果客戶端未發(fā)送冪等鍵頭,則我們可以通過對有效負(fù)載進(jìn)行哈希處理創(chuàng)建一個,并將鍵存儲在Mapper中。(如果訂單ID尚未生成,系統(tǒng)將為每個新請求生成一個)。
- 接收服務(wù)將執(zhí)行重復(fù)檢查,查看訂單是否已存在系統(tǒng)中;如果是,它將返回訂單的當(dāng)前狀態(tài);如果不是,它將持久化訂單及其當(dāng)前狀態(tài)—RCVD。
- 接下來,接收服務(wù)可以進(jìn)行必要的驗證并進(jìn)行支付。如果成功,則加載訂單的當(dāng)前狀態(tài)(因為它可能已經(jīng)是PAID狀態(tài)),如果狀態(tài)是RCVD,則處理支付并更新狀態(tài)為—PAID。如果接收服務(wù)由于網(wǎng)絡(luò)故障或其他原因重試支付現(xiàn)有訂單,我們的狀態(tài)模型將救場,重試將被拒絕,因為訂單已經(jīng)是PAID狀態(tài)。
- 成功支付后,接收服務(wù)將訂單發(fā)送進(jìn)行履行。如果訂單狀態(tài)是PAID,訂單將被履行并達(dá)到最終狀態(tài)—FULFILLED。如果此處發(fā)生重試,狀態(tài)模型將救場。
- 注意,如果接收服務(wù)或支付服務(wù)有多個實例(pods)運行,確保在更新狀態(tài)時擁有集群鎖。
- 另一個要點是,為了加快冪等性的處理,可以使用緩存層存儲映射信息,但在我的案例中,主數(shù)據(jù)存儲足以處理高負(fù)載(避免過早優(yōu)化)。
這是冪等性的一個演示用例。對于生產(chǎn)環(huán)境,你需要考慮你的用例,以及如何將該模型應(yīng)用于實際場景。
結(jié)論
冪等性對于構(gòu)建大規(guī)模、數(shù)據(jù)完整性不受影響的彈性分布式系統(tǒng)至關(guān)重要。冪等性的重試策略是分布式事務(wù)的一個優(yōu)秀替代方案,后者更復(fù)雜且隨著擴展更難以管理。