溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

redis中如何解決分布式冪等問題

發(fā)布時間:2021-11-17 11:04:46 來源:億速云 閱讀:116 作者:小新 欄目:大數(shù)據(jù)

這篇文章給大家分享的是有關(guān)redis中如何解決分布式冪等問題的內(nèi)容。小編覺得挺實(shí)用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。

一 背景

分布式系統(tǒng)由眾多微服務(wù)組成,微服務(wù)之間必然存在大量的網(wǎng)絡(luò)調(diào)用。下圖是一個服務(wù)間調(diào)用異常的例子,用戶提交訂單之后,請求到A服務(wù),A服務(wù)落單之后,開始調(diào)用B服務(wù),但是在A調(diào)用B的過程中,存在很多不確定性,例如B服務(wù)執(zhí)行超時了,RPC直接返回A請求超時了,然后A返回給用戶一些錯誤提示,但實(shí)際情況是B有可能執(zhí)行是成功的,只是執(zhí)行時間過長而已。

redis中如何解決分布式冪等問題

用戶看到錯誤提示之后,往往會選擇在界面上重復(fù)點(diǎn)擊,導(dǎo)致重復(fù)調(diào)用,如果B是個支付服務(wù)的話,用戶重復(fù)點(diǎn)擊可能導(dǎo)致同一個訂單被扣多次錢。不僅僅是用戶可能觸發(fā)重復(fù)調(diào)用,定時任務(wù)、消息投遞和機(jī)器重新啟動都可能會出現(xiàn)重復(fù)執(zhí)行的情況。在分布式系統(tǒng)里,服務(wù)調(diào)用出現(xiàn)各種異常的情況是很常見的,這些異常情況往往會使得系統(tǒng)間的狀態(tài)不一致,所以需要容錯補(bǔ)償設(shè)計,最常見的方法就是調(diào)用方實(shí)現(xiàn)合理的重試策略,被調(diào)用方實(shí)現(xiàn)應(yīng)對重試的冪等策略。

二 什么是冪等

對于冪等,有一個很常見的描述是:對于相同的請求應(yīng)該返回相同的結(jié)果,所以查詢類接口是天然的冪等性接口。舉個例子:如果有一個查詢接口是查詢訂單的狀態(tài),狀態(tài)是會隨著時間發(fā)生變化的,那么在兩次不同時間的查詢請求中,可能返回不一樣的訂單狀態(tài),這個查詢接口還是冪等接口嗎?

冪等的定義直接決定了我們?nèi)绾稳ピO(shè)計冪等方案,如果冪等的含義是相同請求返回相同結(jié)果,那實(shí)際上只需要緩存第一次的返回結(jié)果,即可在后續(xù)重復(fù)請求時實(shí)現(xiàn)冪等了。但問題真的有這么簡單嗎?

筆者更贊同這種定義:冪等指的是相同請求(identical request)執(zhí)行一次或者多次所帶來的副作用(side-effects)是一樣的

引自:https://developer.mozilla.org/en-US/docs/Glossary/Idempotent An HTTP method is idempotent if an identical request can be made once or several times in a row with the same effect while leaving the server in the same state. In other words, an idempotent method should not have any side-effects (except for keeping statistics).

這個定義有一定的抽象,概括性比較強(qiáng),在設(shè)計冪等方案時,其實(shí)就是將抽象部分具化。例如:什么是相同的請求?哪些情況會有副作用?該如何避免副作用?且看三部曲。

三 解決方案三部曲

不少關(guān)于冪等的文章都稱自己的方案是通用解決方案,但筆者卻認(rèn)為,不同的業(yè)務(wù)場景下,相同請求和副作用都是有差異性的,不同的副作用需要不同的方案來解決,不存在完全通用的解決方案。而三部曲旨在提煉出一種思考模式,并舉例說明,在該思考模式下,更容易設(shè)計出符合業(yè)務(wù)場景的冪等解決方案。

第一部曲:識別相同請求

冪等是為了解決重復(fù)執(zhí)行同一請求的問題,那如何識別一個請求有沒有和之前的請求重復(fù)呢?有的方案是通過請求中的某個流水號字段來識別的,同一個流水號表示同一個請求。也有的方案是通過請求中某幾個字段甚至全部字段進(jìn)行比較,從而來識別是否為同一個請求。所以在方案設(shè)計時,明確定義具體業(yè)務(wù)場景下什么是相同請求,這是第一部曲

方案舉例:token機(jī)制識別前端重復(fù)請求

在一條調(diào)用鏈路的后端系統(tǒng)中,一般都可以通過上游系統(tǒng)傳遞的reqNo+source來識別是否是為重復(fù)的請求。如下圖,B系統(tǒng)是依賴于A系統(tǒng)傳遞的reqNo+source來識別相同請求的,但是A系統(tǒng)是直接和前端頁面交互的系統(tǒng),如何識別用戶發(fā)起的請求是相同的呢?比如用戶在支付界面上點(diǎn)擊了多次,A系統(tǒng)怎么識別這是一次重復(fù)操作呢?

redis中如何解決分布式冪等問題

前端可以在第一次點(diǎn)擊完成時,將按鈕設(shè)置為disable,這樣用戶無法在界面上重復(fù)點(diǎn)擊第二次,但這只是提升體驗(yàn)的前端解決方案,不是真正安全的解決方案。

常見的服務(wù)端解決方案是采用token機(jī)制來實(shí)現(xiàn)防重復(fù)提交。如下圖,

redis中如何解決分布式冪等問題

(1)當(dāng)用戶進(jìn)入到表單頁面的時候,前端會從服務(wù)端申請到一個token,并保存在前端。

(2)當(dāng)用戶第一次點(diǎn)擊提交的時候,會將該token和表單數(shù)據(jù)一并提交到服務(wù)端,服務(wù)端判斷該token是否存在,如果存在則執(zhí)行業(yè)務(wù)邏輯。

(3)當(dāng)用戶第二次點(diǎn)擊提交的時候,會將該token和表單數(shù)據(jù)一并提交到服務(wù)端,服務(wù)端判斷該token是否存在,如果不存在則返回錯誤,前端顯示提交失敗。

這個方案結(jié)合前后端,從前端視角,這是用于防止重復(fù)請求,從服務(wù)端視角,這個用于識別前端相同請求。服務(wù)端往往基于類似于redis之類的分布式緩存來實(shí)現(xiàn),保證生成token的唯一性和操作token時的原子性即可。核心邏輯如下。

// SETNX keyName value: 如果key存在,則返回0,如果不存在,則返回1

// step1. 申請token
String token = generateUniqueToken();

// step2. 校驗(yàn)token是否存在
if(redis.setNx(token, 1) == 1){
  // do business
} else {
 // 冪等邏輯
}

第二部曲:列出并減少副作用的分析維度

相同的請求重復(fù)執(zhí)行業(yè)務(wù)邏輯,如果處理不當(dāng),會給系統(tǒng)帶來副作用。那什么是副作用?就是業(yè)務(wù)無法接受的非預(yù)期結(jié)果。最常見的有重復(fù)入庫、數(shù)據(jù)被錯誤變更等,大多數(shù)冪等方案就是圍繞解決這類問題來設(shè)計的。而系統(tǒng)往往可能在多個維度都存在副作用,例如:

(1)調(diào)用下游維度:重復(fù)調(diào)用下游會怎樣?如果下游沒有冪等,重復(fù)調(diào)用會帶來什么副作用?

(2)返回上游維度:例如第一次返回上游異常,第二次返回上游被冪等了?會給上游帶來什么副作用?

(3)并發(fā)執(zhí)行維度:并發(fā)重復(fù)執(zhí)行會怎樣?會有什么副作用?

(4)分布式鎖維度:引入分布式鎖來防止并發(fā)執(zhí)行?但是如果鎖出現(xiàn)不一致性,會有什么副作用?

(5)交互時序維度:有沒有異步交互,是否存在時序問題?會有什么副作用?

(6)客戶體驗(yàn)維度:從數(shù)據(jù)不一致到最終一致,必須在多少時間內(nèi)完成?如果該時間內(nèi)沒有完成,會有什么副作用?例如大量客訴(秉承客戶第一的原則,在支付寶,客訴量太大會定級為生產(chǎn)環(huán)境故障)。

(7)業(yè)務(wù)核對維度:重復(fù)調(diào)用是否存在覆蓋核對標(biāo)識的情況,帶來無法正常核對的副作用?在金融系統(tǒng)中,資金鏈路無法核對是無法接受的。

(8)數(shù)據(jù)質(zhì)量維度:是否存在重復(fù)記錄?如果存在會有什么副作用?

redis中如何解決分布式冪等問題

上面是一些常見的分析維度,不同行業(yè)的系統(tǒng)中會存在不一樣的維度,盡可能地總結(jié)出這些維度,并列入系統(tǒng)分析時的checklist中,能夠更好地完善冪等解決方案。沒有副作用才算是完備的冪等解決方案,但是副作用的維度太多,會提高冪等方案的復(fù)雜度。所以在能夠達(dá)成業(yè)務(wù)的前提下,減少一些分析維度,能夠使得冪等方案實(shí)現(xiàn)起來更加經(jīng)濟(jì)有效。例如:如果有專門的冪等表存儲返回給上游的冪等結(jié)果,第(2)維度不用考慮了,如果用鎖來防止并發(fā),第(3)個維度不考慮了,如果用單機(jī)鎖代替分布式鎖,第(4)個維度不考慮了。

這是解決冪等問題的第二部曲:列出并減少副作用的分析維度。在這部曲中,涉及的解決方案往往是解決某一個維度的副作用問題,適合以通用組件的形式存在,作為團(tuán)隊內(nèi)部的一個公共技術(shù)套路。

方案舉例:加鎖避免并發(fā)重復(fù)執(zhí)行

很多冪等解決方案都和防并發(fā)有關(guān),那么冪等和并發(fā)到底有什么關(guān)聯(lián)呢?兩者的聯(lián)系是:冪等解決的是重復(fù)執(zhí)行的問題,重復(fù)執(zhí)行既有串行重復(fù)執(zhí)行(例如定時任務(wù)),也有并發(fā)重復(fù)執(zhí)行。如果重復(fù)執(zhí)行的業(yè)務(wù)邏輯沒有共享變量和數(shù)據(jù)變更操作時,并發(fā)重復(fù)執(zhí)行是沒有副作用的,可以不考慮并發(fā)的問題。對于包含共享變量、涉及變更操作的服務(wù)(實(shí)際上這類服務(wù)居多),并發(fā)問題可能導(dǎo)致亂序讀寫共享變量,重復(fù)插入數(shù)據(jù)等問題。特別是并發(fā)讀寫共享變量,往往都是發(fā)生生產(chǎn)故障后才被感知到。

所以在并發(fā)執(zhí)行的維度,將并發(fā)重復(fù)執(zhí)行變成串行重復(fù)執(zhí)行是最好的冪等解決方案。支付寶最常見的方法就是:一鎖二判三更新,如下圖。當(dāng)一個請求過來之后:一鎖,鎖住要操作的資源;二判,識別是否為重復(fù)請求(第一部曲要定義的問題)、判斷業(yè)務(wù)狀態(tài)是否正常;三更新:執(zhí)行業(yè)務(wù)邏輯。

redis中如何解決分布式冪等問題

小A:鎖可能造成性能影響,先判后鎖再執(zhí)行,可以提升效能。 大明:這樣可能會失去防并發(fā)的效果。還記得double check實(shí)現(xiàn)單例模式嗎?在加鎖前判斷了下,那加鎖后為啥還要判斷下?實(shí)際上第二次check才是必須的。想想看? 小A畫圖思考中... 小A:明白了,一鎖二判三更新,鎖和判的順序是不能變的,如果鎖沖突比較高,可以在鎖之前判斷下,提高效率,所以稱之為double check。 大明:是的,聰明。這兩個場景不一樣,但并發(fā)思路是一樣的。

private volatile static Girl theOnlyGirl;

// 實(shí)現(xiàn)單例時做了 double check
public static Girl getTheOnlyGirl() {

    if (theOnlyGirl == null) {   // 加鎖前check
        synchronized (Girl.class) {
            if (theOnlyGirl == null) {  // 加鎖后check
                theOnlyGirl = new Girl();    // 變更執(zhí)行
            }
        }
    }

    return theOnlyGirl;
}

鎖的實(shí)現(xiàn)可以是分布式鎖,也是可以是數(shù)據(jù)庫鎖。分布式鎖本身會帶來鎖的一致性問題,需要根據(jù)業(yè)務(wù)對系統(tǒng)穩(wěn)定性的要求來考量。支付寶的很多系統(tǒng)是通過在業(yè)務(wù)數(shù)據(jù)庫中新建一個鎖記錄表來實(shí)現(xiàn)業(yè)務(wù)鎖組件,其分表邏輯和業(yè)務(wù)表的分表邏輯一致,就可以實(shí)現(xiàn)單機(jī)數(shù)據(jù)庫鎖。如果沒有鎖組件,悲觀鎖鎖住業(yè)務(wù)單據(jù)也是可以滿足條件的,悲觀鎖要在事務(wù)中用select for update來實(shí)現(xiàn),要注意死鎖問題,且where條件中必須命中索引,否則會鎖表,不鎖記錄。

并發(fā)維度幾乎是一個分布式冪等的通用分析維度,所以一個通用的鎖組件是很有必要的。但這也只是解決了并發(fā)這一個維度的副作用。雖然沒有了并發(fā)重復(fù)執(zhí)行的情況,但串行重復(fù)執(zhí)行的情況依舊存在,重復(fù)執(zhí)行才是冪等核心要解決的問題,重復(fù)執(zhí)行如果還存在其它副作用,冪等問題就是沒有解決掉。

加鎖后業(yè)務(wù)的性能會降低,這個怎么解決?筆者認(rèn)為,大多數(shù)情況下架構(gòu)的穩(wěn)定性比系統(tǒng)性能的優(yōu)先級更高,況且對于性能的優(yōu)化有太多地方可以去實(shí)現(xiàn),減少壞代碼、去除慢SQL、優(yōu)化業(yè)務(wù)架構(gòu)、水平擴(kuò)展數(shù)據(jù)庫資源等方式。通過系統(tǒng)壓測來實(shí)現(xiàn)一個滿足SLA的服務(wù)才是評估全鏈路性能的正確方法。

第三部:識別細(xì)粒度副作用,針對性設(shè)計解決方案

在解決了部分維度的副作用之后,就需要針對單個粒度的副作用進(jìn)行逐一識別并解決了。在數(shù)據(jù)質(zhì)量維度上,最大的一個副作用是重復(fù)數(shù)據(jù)。在交互維度上,最大的一個副作用是業(yè)務(wù)亂序執(zhí)行。一般這類問題不設(shè)計成通用組件,可以開發(fā)人員自由發(fā)揮。本節(jié)用兩個常見方案做為例子。

方案舉例1:唯一性約束避免重復(fù)落庫

在數(shù)據(jù)表設(shè)計時,設(shè)計兩個字段:source、reqNo,source表示調(diào)用方,seqNo表示調(diào)用方發(fā)送過來的請求號。source和reqNo設(shè)置為組合唯一索引,保證單據(jù)不會重復(fù)落兩次。如果調(diào)用方?jīng)]有source和reqNo這兩個字段,可以根據(jù)業(yè)務(wù)實(shí)際情況將請求中的某幾個業(yè)務(wù)參數(shù)生成一個md5作為唯一性字段落到唯一性字段中來避免重復(fù)落庫。

redis中如何解決分布式冪等問題

核心邏輯如下:

try {
    dao.insert(entity);    
    // do business
} catch (DuplicateKeyException e) {
    dao.select(param);
    // 冪等返回
}

這里直接insert單據(jù),若果成功則表示沒請求過,舉行執(zhí)行業(yè)務(wù)邏輯,如果拋出DuplicateKeyException異常,則表示已經(jīng)執(zhí)行過,做冪等返回,簡單的服務(wù)通過這種方式也可以識別是否為重復(fù)請求(第一部曲)。

利用數(shù)據(jù)庫唯一索引來避免重復(fù)記錄,需要注意以下幾個問題:

(1)因?yàn)榇嬖谧x寫分離的設(shè)計,有可能insert操作的是主庫,但select查詢的卻是從庫,如果主備同步不及時,有可能select查出來也是空的。

(2)在數(shù)據(jù)庫有Failover機(jī)制的情況下,如果一個城市出現(xiàn)自然災(zāi)害,很可能切換到另外一個城市的備用庫,那么唯一性約束可能就會出現(xiàn)失效的情況,比如并發(fā)場景下第一次insert是在杭州的庫,然后此時failover將庫切到上海了,再一次同樣的請求insert也是成功的。

(3)數(shù)據(jù)庫擴(kuò)容場景下,因?yàn)榉謳煲?guī)則發(fā)生變化,有可能第一次insert操作是在A庫,第二次insert操作是在B庫,唯一索引同樣不起作用。

(4)有的系統(tǒng)catch的是SQLIntegrityConstraintViolationException,這個是完整性約束,包含了唯一性約束,如果未給一個必填字段設(shè)值,也會拋這個異常,所以應(yīng)該catch鍵重復(fù)異常DuplicateKeyException。

對于第(1)個問題,將insert 和select放在同一個事務(wù)中即可解決,對于(2)和(3),支付寶內(nèi)部為了應(yīng)對容量暴漲和FO,設(shè)計了一套基于數(shù)據(jù)復(fù)制技術(shù)的分布式數(shù)據(jù)平臺,這個case筆者了解不深,后續(xù)有機(jī)會再討論。

小A:如果我用唯一性約束來保證不會落重復(fù)數(shù)據(jù),是不是可以不加鎖防并發(fā)了? 大明:兩者沒有直接關(guān)系,加鎖防并發(fā)解決的是并發(fā)維度的副作用問題,唯一性約束只是解決重復(fù)數(shù)據(jù)這單個副作用的問題。如果沒有唯一性約束,串行重復(fù)執(zhí)行也會導(dǎo)致insert重復(fù)落數(shù)據(jù)的問題,唯一性約束本質(zhì)上解決的是重復(fù)數(shù)據(jù)問題,不是并發(fā)問題。

方案舉例2:狀態(tài)機(jī)約束解決亂序問題

一個業(yè)務(wù)的生命周期往往存在不同的狀態(tài),用狀態(tài)機(jī)來控制業(yè)務(wù)流程中的狀態(tài)轉(zhuǎn)換是不二之選。在實(shí)際業(yè)務(wù)中單向的狀態(tài)機(jī)是比較常用的,當(dāng)狀態(tài)機(jī)處于下一個狀態(tài)時,是不能回到前面的狀態(tài)的。以下場景經(jīng)常會用到狀態(tài)機(jī)做校驗(yàn):

(1)調(diào)用方調(diào)用超時重試。

(2)消息投遞超時重試。

(3)業(yè)務(wù)系統(tǒng)發(fā)起多個任務(wù),但是期待按照發(fā)起順序有序返回。

對于這種類問題,一般是在處理前先判斷狀態(tài)是否符合預(yù)期,如果符合預(yù)期再執(zhí)行業(yè)務(wù)。當(dāng)業(yè)務(wù)執(zhí)行完成后,變更狀態(tài)時還會采取類似于于樂觀鎖的方式兜底校驗(yàn),例如,M狀態(tài)只能從N狀態(tài)轉(zhuǎn)換而來,那么更新單據(jù)時,會在sql中做狀態(tài)校驗(yàn)。

update apply set status = 'M' where status = 'N'

如果狀態(tài)被設(shè)計成可逆的,就有可能產(chǎn)生ABA問題。即在update之前,狀態(tài)有可能做過這樣的變更:N -> M -> N。所以狀態(tài)機(jī)設(shè)成單向流轉(zhuǎn)是比較合理的。

四 總結(jié)

本文首先引出了冪等的定義:相同請求無副作用,然后提出了設(shè)計冪等方案的三部曲,并舉例說明。設(shè)計者要能夠清晰地定義相同請求,并且采用通用組件減少一些副作用的分析維度,再針對具體的副作用設(shè)計相應(yīng)的解決方案,直至沒有任何副作用,才是真正完備的冪等解決方案。在實(shí)際業(yè)務(wù)中,實(shí)現(xiàn)三部曲不一定是嚴(yán)格的先后順序,但只要按照這三部曲來構(gòu)思方案,必能開拓思路,化繁為簡。 redis中如何解決分布式冪等問題

感謝各位的閱讀!關(guān)于“redis中如何解決分布式冪等問題”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,讓大家可以學(xué)到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI