跟限流框架類似,冪等框架的功能性需求也比較簡(jiǎn)單,但要考慮處理的異常情況有很多渡处,比如業(yè)務(wù)代碼異常镜悉、業(yè)務(wù)系統(tǒng)宕機(jī)、冪等框架異常医瘫。本文侣肄,我們重點(diǎn)講解如何應(yīng)對(duì)這些異常情況,設(shè)計(jì)一個(gè)高度容錯(cuò)的冪等框架醇份。
冪等處理正常流程
- 調(diào)用方從發(fā)起接口請(qǐng)求到接收到響應(yīng)稼锅,一般要經(jīng)過三個(gè)階段。第一個(gè)階段是調(diào)用方發(fā)送請(qǐng)求并被實(shí)現(xiàn)方接收僚纷,第二個(gè)階段是執(zhí)行接口對(duì)應(yīng)的業(yè)務(wù)邏輯矩距,第三個(gè)階段是將執(zhí)行結(jié)果返回給調(diào)用方。為了實(shí)現(xiàn)接口冪等怖竭,我們需要將冪等相關(guān)的邏輯锥债,添加在這三個(gè)階段中。
- 正常情況下痊臭,冪等號(hào)隨著請(qǐng)求傳遞到接口實(shí)現(xiàn)方之后哮肚,接口實(shí)現(xiàn)方將冪等號(hào)解析出來,傳遞給冪等框架趣兄。冪等框架先去數(shù)據(jù)庫(kù)(比如 Redis)中查找這個(gè)冪等號(hào)是否已經(jīng)存在绽左。如果存在,說明業(yè)務(wù)邏輯已經(jīng)或者正在執(zhí)行艇潭,就不要重復(fù)執(zhí)行了拼窥。如果冪等號(hào)不存在,就將冪等號(hào)存儲(chǔ)在數(shù)據(jù)庫(kù)中蹋凝,然后再執(zhí)行相應(yīng)的業(yè)務(wù)邏輯鲁纠。
- 正常情況下,冪等處理流程是非常簡(jiǎn)單的鳍寂,難點(diǎn)在于如何應(yīng)對(duì)異常情況改含。在這三個(gè)階段中,如果第一個(gè)階段出現(xiàn)異常迄汛,比如發(fā)送請(qǐng)求失敗或者超時(shí)捍壤,冪等號(hào)還沒有記錄下來,重試請(qǐng)求會(huì)被執(zhí)行鞍爱,符合我們的預(yù)期鹃觉。如果第三個(gè)階段出現(xiàn)異常,業(yè)務(wù)邏輯執(zhí)行完成了睹逃,只是在發(fā)送結(jié)果給調(diào)用方的時(shí)候盗扇,失敗或者超時(shí)了,這個(gè)時(shí)候,冪等號(hào)已經(jīng)記錄下來疗隶,重試請(qǐng)求不會(huì)被執(zhí)行佑笋,也符合我們的預(yù)期。也就是說斑鼻,第一蒋纬、第三階段出現(xiàn)異常,上述的冪等處理邏輯都可以正確應(yīng)對(duì)卵沉。
- 但是颠锉,如果第二個(gè)階段業(yè)務(wù)執(zhí)行的過程出現(xiàn)異常法牲,處理起來就復(fù)雜多了史汗。接下來,我們就看下冪等框架該如何應(yīng)對(duì)這一階段的各種異常拒垃。這里分了三類異常來講解停撞,它們分別是業(yè)務(wù)代碼異常、業(yè)務(wù)系統(tǒng)宕機(jī)悼瓮、冪等框架異常戈毒。
業(yè)務(wù)代碼異常處理
- 當(dāng)業(yè)務(wù)代碼在執(zhí)行過程中拋出異常的時(shí)候,我們是否應(yīng)該認(rèn)定為業(yè)務(wù)處理失敗横堡,然后將已經(jīng)記錄的冪等號(hào)刪除埋市,允許重新執(zhí)行業(yè)務(wù)邏輯呢?
- 對(duì)于這個(gè)問題命贴,我們要分業(yè)務(wù)異常和系統(tǒng)異常來區(qū)分對(duì)待道宅。那什么是業(yè)務(wù)異常?什么是系統(tǒng)異常呢胸蛛?舉個(gè)例子解釋一下污茵。比如,A 用戶發(fā)送消息給 B 用戶葬项,但是查詢 B 用戶不存在泞当,拋出 UserNotExisting 異常,我們把這種業(yè)務(wù)上不符合預(yù)期叫做業(yè)務(wù)異常民珍。因?yàn)閿?shù)據(jù)庫(kù)掛掉了襟士,業(yè)務(wù)代碼訪問數(shù)據(jù)庫(kù)時(shí),就會(huì)報(bào)告數(shù)據(jù)庫(kù)異常嚷量,我們把這種非業(yè)務(wù)層面的陋桂、系統(tǒng)級(jí)的異常,叫做系統(tǒng)異常津肛。
- 遇到業(yè)務(wù)異常(比如 UserNotExisting 異常)章喉,我們不刪除已經(jīng)記錄的冪等號(hào),不允許重新執(zhí)行同樣的業(yè)務(wù)邏輯,因?yàn)樵俅沃匦聢?zhí)行也是徒勞的秸脱,還是會(huì)報(bào)告異常落包。相反,遇到系統(tǒng)異常(比如數(shù)據(jù)庫(kù)訪問異常)摊唇,我們將已經(jīng)記錄的冪等號(hào)刪除咐蝇,允許重新執(zhí)行這段業(yè)務(wù)邏輯。因?yàn)樵谙到y(tǒng)級(jí)問題修復(fù)之后(比如數(shù)據(jù)庫(kù)恢復(fù)了)巷查,重新執(zhí)行之前失敗的業(yè)務(wù)邏輯有序,就有可能會(huì)成功。
- 實(shí)際上岛请,為了讓冪等框架盡可能的靈活旭寿,低侵入業(yè)務(wù)邏輯,發(fā)生異常(不管是業(yè)務(wù)異常還是系統(tǒng)異常)崇败,是否允許再重試執(zhí)行業(yè)務(wù)邏輯盅称,交給開發(fā)這塊業(yè)務(wù)的工程師來決定是最合適的了,畢竟他最清楚針對(duì)每個(gè)異常該如何處理后室。而冪等框架本身不參與這個(gè)決定缩膝,它只需要提供刪除冪等號(hào)的接口,由業(yè)務(wù)工程師來決定遇到異常的時(shí)候岸霹,是否需要調(diào)用這個(gè)刪除接口疾层,刪除已經(jīng)記錄的冪等號(hào)。
業(yè)務(wù)系統(tǒng)宕機(jī)處理
- 剛剛分析的是代碼異常贡避,我們?cè)賮砜聪峦蠢瑁绻跇I(yè)務(wù)處理的過程中,業(yè)務(wù)系統(tǒng)宕機(jī)了(你可以簡(jiǎn)單理解為部署了業(yè)務(wù)系統(tǒng)的機(jī)器宕機(jī)了)贸桶,冪等框架是否還能正確工作呢舅逸?
- 如果冪等號(hào)已經(jīng)記錄下了,但是因?yàn)闄C(jī)器宕機(jī)皇筛,業(yè)務(wù)還沒來得及執(zhí)行琉历,按照剛剛的冪等框架的處理流程,即便機(jī)器重啟水醋,業(yè)務(wù)也不會(huì)再被觸發(fā)執(zhí)行了旗笔,這個(gè)時(shí)候該怎么辦呢?除此之外拄踪,如果記錄冪等號(hào)成功了蝇恶,但是在捕獲到系統(tǒng)異常之后,要?jiǎng)h除冪等號(hào)之前惶桐,機(jī)器宕機(jī)了撮弧,這個(gè)時(shí)候又該怎么辦潘懊?
- 如果希望冪等號(hào)的記錄和業(yè)務(wù)的執(zhí)行完全一致,我們就要把它們放到一個(gè)事務(wù)中贿衍。執(zhí)行成功授舟,必然會(huì)記錄冪等號(hào);執(zhí)行失敗贸辈,冪等號(hào)記錄也會(huì)被自動(dòng)回滾释树。因?yàn)閮绲瓤蚣芎蜆I(yè)務(wù)系統(tǒng)各自使用獨(dú)立的數(shù)據(jù)庫(kù)來記錄數(shù)據(jù),所以擎淤,這里涉及的事務(wù)屬于分布式事務(wù)奢啥。如果為了解決這個(gè)問題,引入分布式事務(wù)嘴拢,那冪等框架的開發(fā)難度提高了很多桩盲,并且框架使用起來也復(fù)雜了很多,性能也會(huì)有所損失炊汤。
- 針對(duì)這個(gè)問題正驻,我們還有另外一種解決方案。那就是抢腐,在存儲(chǔ)業(yè)務(wù)數(shù)據(jù)的業(yè)務(wù)數(shù)據(jù)庫(kù)( 比如 MySQL)中,建一張表來記錄冪等號(hào)襟交。冪等號(hào)先存儲(chǔ)到業(yè)務(wù)數(shù)據(jù)庫(kù)中迈倍,然后再同步給冪等框架的 Redis 數(shù)據(jù)庫(kù)。這樣做的好處是捣域,我們不需要引入分布式事務(wù)框架啼染,直接利用業(yè)務(wù)數(shù)據(jù)庫(kù)本身的事務(wù)屬性,保證業(yè)務(wù)數(shù)據(jù)和冪等號(hào)的寫入操作焕梅,要么都成功迹鹅,要么都失敗。不過贞言,這個(gè)解決方案會(huì)導(dǎo)致冪等邏輯斜棚,跟業(yè)務(wù)邏輯沒有完全解耦,不符合我們之前講到的低侵入该窗、松耦合的設(shè)計(jì)思想弟蚀。
- 實(shí)際上,做工程不是做理論酗失。對(duì)于這種極少發(fā)生的異常义钉,在工程中,我們能夠做到规肴,在出錯(cuò)時(shí)能及時(shí)發(fā)現(xiàn)問題捶闸、能夠根據(jù)記錄的信息人工修復(fù)就可以了夜畴。雖然看起來解決方案不優(yōu)雅,不夠智能删壮,不夠自動(dòng)化斩启,但是,這比編寫一大坨復(fù)雜的代碼邏輯來解決醉锅,要好使得多兔簇。所以,我們建議業(yè)務(wù)系統(tǒng)記錄 SQL 的執(zhí)行日志硬耍,在日志中附加上冪等號(hào)垄琐。這樣我們就能在機(jī)器宕機(jī)時(shí),根據(jù)日志來判斷業(yè)務(wù)執(zhí)行情況和冪等號(hào)的記錄是否一致经柴。
冪等框架異常處理
- 我們前面提到狸窘,限流框架本身的異常,不能導(dǎo)致接口響應(yīng)異常坯认。那對(duì)于冪等框架來說翻擒,是否也適用這條設(shè)計(jì)原則呢?
- 對(duì)于限流來說牛哺,限流框架執(zhí)行異常(比如陋气,Redis 訪問超時(shí)或者訪問失敗)引润,我們可以觸發(fā)服務(wù)降級(jí)巩趁,讓限流功能暫時(shí)不起作用,接口還能正常執(zhí)行淳附。如果大量的限流接口調(diào)用異常议慰,在具有完善監(jiān)控的情況下,這些異常很快就會(huì)被運(yùn)維發(fā)現(xiàn)并且修復(fù)奴曙,所以别凹,短暫的限流失效,也不會(huì)對(duì)業(yè)務(wù)系統(tǒng)產(chǎn)生太多影響洽糟。畢竟限流只是一個(gè)針對(duì)突發(fā)情況的保護(hù)機(jī)制炉菲,平時(shí)并不起作用。如果偶爾的極個(gè)別的限流接口調(diào)用異常脊框,本不應(yīng)該被放過的幾個(gè)接口請(qǐng)求颁督,因?yàn)橄蘖鞯臅簳r(shí)失效被放過了,對(duì)于這種情況浇雹,絕大部分業(yè)務(wù)場(chǎng)景都是可以接受的沉御。畢竟限流不可能做到非常精確,多放過一兩個(gè)接口請(qǐng)求幾乎沒影響昭灵。
- 對(duì)于冪等來說吠裆,盡管它應(yīng)對(duì)的也是超時(shí)重試等特殊場(chǎng)景伐谈,但是,如果本不應(yīng)該重新執(zhí)行的業(yè)務(wù)邏輯试疙,因?yàn)閮绲裙δ艿臅簳r(shí)失效诵棵,被重復(fù)執(zhí)行了,就會(huì)導(dǎo)致業(yè)務(wù)出錯(cuò)(比如祝旷,多次執(zhí)行轉(zhuǎn)賬履澳,錢多轉(zhuǎn)了)。對(duì)于這種情況怀跛,絕大部分業(yè)務(wù)場(chǎng)景都是無法接受的距贷。所以,在冪等邏輯執(zhí)行異常時(shí)吻谋,我們選擇讓接口請(qǐng)求也失敗忠蝗,相應(yīng)的業(yè)務(wù)邏輯就不會(huì)被重復(fù)執(zhí)行了。畢竟接口請(qǐng)求失斃焓啊(比如轉(zhuǎn)錢沒轉(zhuǎn)成功)阁最,比業(yè)務(wù)執(zhí)行出錯(cuò)(比如多轉(zhuǎn)了錢),修復(fù)的成本要低很多骇两。
小結(jié)
- 本文分了三類異常來對(duì)冪等框架進(jìn)行講解
- 對(duì)于業(yè)務(wù)代碼異常速种,為了讓冪等框架盡可能的靈活,低侵入業(yè)務(wù)邏輯脯颜,發(fā)生異常(不管是業(yè)務(wù)異常還是系統(tǒng)異常)哟旗,是否允許再重試執(zhí)行業(yè)務(wù)邏輯,交給開發(fā)這塊業(yè)務(wù)的工程師來決定栋操。
- 對(duì)于業(yè)務(wù)系統(tǒng)宕機(jī),對(duì)于這種極少發(fā)生的異常饱亮,在工程中矾芙,我們能夠做到,在出錯(cuò)時(shí)能及時(shí)發(fā)現(xiàn)問題近上、能夠根據(jù)記錄的信息人工修復(fù)剔宪,就可以了。所以壹无,我們建議業(yè)務(wù)系統(tǒng)記錄 SQL 的執(zhí)行日志葱绒,在日志中附加上冪等號(hào)。這樣我們就能在機(jī)器宕機(jī)時(shí)斗锭,根據(jù)日志來判斷業(yè)務(wù)執(zhí)行情況和冪等號(hào)的記錄是否一致地淀。
- 對(duì)于冪等框架異常,跟限流框架異常處理對(duì)策不同岖是,在冪等邏輯執(zhí)行異常時(shí)帮毁,我們選擇讓接口請(qǐng)求也失敗实苞,相應(yīng)的業(yè)務(wù)邏輯就不會(huì)被重復(fù)執(zhí)行了,業(yè)務(wù)就不會(huì)出錯(cuò)烈疚。畢竟接口請(qǐng)求失敗黔牵,比業(yè)務(wù)執(zhí)行出錯(cuò),修復(fù)的成本要低很多爷肝。
- 雖然冪等框架要處理的異常很多猾浦,但考慮到開發(fā)成本以及簡(jiǎn)單易用性,我們對(duì)某些異常的處理在工程上做了妥協(xié)灯抛,交由業(yè)務(wù)系統(tǒng)或者人工介入處理金赦。這樣就大大簡(jiǎn)化了冪等框架開發(fā)的復(fù)雜度和難度。