聊聊冪等我們先看看這樣一個(gè)問(wèn)題,。 如果你去食堂吃飯,。你沖食堂窗口里喊了一聲:“師傅,倆包子,!”等了半天,,你還沒(méi)見(jiàn)著你的包子。于是,,你又沖食堂窗口里喊了一聲:“師傅,,倆包子!” 這時(shí)候,你希望食堂師傅怎么做,? A:給你倆包子,。 B:給你四個(gè)包子。 C:答應(yīng)一聲“來(lái)了”,,但什么也不給你,。 D:端來(lái)一碗粥,扣在你臉上,。 正常人應(yīng)該都會(huì)選A,。這就是冪等。 (說(shuō)選B的那個(gè),,下回我說(shuō)完兩次“你欠我10塊錢(qián)”后,,請(qǐng)你記得還我20。) 什么是冪等冪等的各種數(shù)學(xué)定義我們就不討論了,。一般來(lái)說(shuō),,我們都會(huì)這樣定義冪等: 冪等是指:在其它條件不變的情況下,用同樣的數(shù)據(jù),、執(zhí)行同樣的方法,,執(zhí)行一次和執(zhí)行一萬(wàn)次,都能得到同樣的結(jié)果,。 例如,,在沒(méi)有人修改數(shù)據(jù)庫(kù)數(shù)據(jù)的情況下執(zhí)行查詢操作,用同樣的SQL查詢一次和查詢一萬(wàn)次,,都應(yīng)該能得到同樣的數(shù)據(jù),。 幾個(gè)注意點(diǎn)冪等操作看起來(lái)簡(jiǎn)單,其實(shí)有很多需要特別注意的地方,。 發(fā)生異常時(shí),,要不要冪等?我們來(lái)看這樣的場(chǎng)景:如果執(zhí)行某方法時(shí),,方法內(nèi)部拋出了異常,;然后,我們修復(fù)了方法內(nèi)部的問(wèn)題,;此時(shí),,調(diào)用方再來(lái)調(diào)用,如果按冪等的要求,,我們難道要再給調(diào)用方拋出一個(gè)異常,、或者返回對(duì)應(yīng)的錯(cuò)誤碼嗎? 這個(gè)問(wèn)題,,除了考慮冪等之外,,我們還需要考慮一點(diǎn):業(yè)務(wù)操作的事務(wù)性,,即業(yè)務(wù)事務(wù)。 業(yè)務(wù)事務(wù)與數(shù)據(jù)庫(kù)事務(wù)類(lèi)似,,同樣應(yīng)當(dāng)有原子性,、一致性、隔離性和持久性,。因?yàn)檫@個(gè)緣故,,我們常常會(huì)把一個(gè)業(yè)務(wù)事務(wù)綁定到一個(gè)數(shù)據(jù)庫(kù)事務(wù)上:只要數(shù)據(jù)庫(kù)事務(wù)提交成功了,這個(gè)業(yè)務(wù)事務(wù)就成功了,。 不過(guò),,業(yè)務(wù)事務(wù)的范疇比數(shù)據(jù)庫(kù)事務(wù)要更大一些:它不僅包括這個(gè)業(yè)務(wù)操作中的數(shù)據(jù)庫(kù)操作,還可能包括三方接口調(diào)用,、MQ消息發(fā)送,、緩存寫(xiě)操作等其它操作。當(dāng)然,,要保證所有操作、尤其是分布式操作的事務(wù)性四要素,,在具體實(shí)現(xiàn)上會(huì)變得非常復(fù)雜,。但是,我們至少在設(shè)計(jì)層面上,、至少在數(shù)據(jù)一致性上,,要保證整個(gè)業(yè)務(wù)操作的事務(wù)性。 那么,,從業(yè)務(wù)事務(wù)的數(shù)據(jù)一致性角度來(lái)考慮,,當(dāng)一個(gè)業(yè)務(wù)操作的中途拋出異常的時(shí)候,我們應(yīng)當(dāng)怎么辦呢,? 顯然,,我們應(yīng)當(dāng)回滾當(dāng)前業(yè)務(wù)事務(wù),撤銷(xiāo)異常前的所有操作結(jié)果,。否則的話,,異常前半部分操作成功,寫(xiě)入了部分?jǐn)?shù)據(jù),;異常后半部分沒(méi)有執(zhí)行,,沒(méi)有寫(xiě)入數(shù)據(jù)。此時(shí),,就會(huì)出現(xiàn)數(shù)據(jù)不一致問(wèn)題,。 回滾完成后,對(duì)我們的系統(tǒng)來(lái)說(shuō),,這個(gè)請(qǐng)求的執(zhí)行次數(shù)是0次,。此時(shí),,調(diào)用方再用同樣的數(shù)據(jù)來(lái)調(diào)用我們——雖然對(duì)調(diào)用方來(lái)說(shuō),這是第二次請(qǐng)求,;但是對(duì)我們來(lái)說(shuō),,從業(yè)務(wù)事務(wù)和冪等的角度來(lái)說(shuō),這就是第一次調(diào)用,。 既然是第一次請(qǐng)求,,那就直接放進(jìn)來(lái),正常操作就好了,。 冪等的依據(jù)是什么,?冪等有三個(gè)條件:其它條件不變、同樣的數(shù)據(jù),、同樣的方法,。這三個(gè)條件中,“同樣的數(shù)據(jù)”是最難判斷的:調(diào)用方傳過(guò)來(lái)一個(gè)a=1,,我們?cè)趺粗肋@是一筆新的業(yè)務(wù)操作,、還是某次操作的重放呢? 在很多接口的請(qǐng)求報(bào)文中,,我們都定義了“請(qǐng)求流水號(hào)”這樣一個(gè)字段,。大多數(shù)時(shí)候,我們就直接用這個(gè)字段來(lái)做冪等依據(jù),。 這種處理方式確實(shí)簡(jiǎn)單易行,。但它其實(shí)是有隱患的:即使兩次請(qǐng)求的業(yè)務(wù)數(shù)據(jù)完全不同,但只要流水號(hào)相同,,那第二次請(qǐng)求就是冪等請(qǐng)求,;即使兩次請(qǐng)求的業(yè)務(wù)數(shù)據(jù)完全相同,但只要流水號(hào)不同,,那第二次請(qǐng)求就是一次新的請(qǐng)求,。 這個(gè)問(wèn)題的直接原因是我們把重要的業(yè)務(wù)約束交給了很可能不可控、不可靠的調(diào)用方,。根子上來(lái)說(shuō),,這個(gè)問(wèn)題的原因在于這個(gè)“請(qǐng)求流水號(hào)”是一個(gè)業(yè)務(wù)無(wú)關(guān)的“唯一鍵”:它并不能真正用來(lái)唯一的標(biāo)識(shí)一筆“業(yè)務(wù)數(shù)據(jù)”。 所以,,比較嚴(yán)謹(jǐn)?shù)膬绲确绞绞钦页稣?qǐng)求數(shù)據(jù)中的業(yè)務(wù)唯一鍵,,以業(yè)務(wù)唯一鍵為依據(jù)來(lái)做冪等。有時(shí)候唯一鍵需要組合太多字段,,做唯一判斷時(shí)不好處理,,我們也可以把這些字段拼接起來(lái)、計(jì)算一次MD5:這個(gè)MD5,,雖然無(wú)法回溯到業(yè)務(wù)數(shù)據(jù)上,,但它的確是一個(gè)業(yè)務(wù)相關(guān)的鍵值,。 不過(guò),這種業(yè)務(wù)唯一鍵確實(shí)不好找,,有的時(shí)候業(yè)務(wù)邏輯確實(shí)沒(méi)有唯一性:我確實(shí)可以在吃完倆包子后,,又去食堂窗口喊一次“師傅,倆包子”,;甚至吃完這倆之后還可以再來(lái)一次“師傅,,倆包子”“。而在這種情況下,,師傅確實(shí)應(yīng)該給我總共六個(gè)包子,。 這種情況下就沒(méi)什么辦法了,雖然不是最佳方案,,但使用接口傳入的流水號(hào),、唯一鍵等特殊字段作為冪等依據(jù)總歸也是一種方案。只不過(guò)這種時(shí)候,,我們必須嚴(yán)格要求調(diào)用方按約束傳值,。 冪等時(shí)只需要返回返回碼嗎?大多數(shù)接口的返回報(bào)文中,,都有code和msg字段,,用來(lái)標(biāo)志本次操作的狀態(tài)。對(duì)這種接口來(lái)說(shuō),,冪等時(shí)顯然只返回code和msg就夠了。 需要注意的是,,這里的code和msg應(yīng)當(dāng)和第一次正常請(qǐng)求時(shí)的一樣,。如果正常請(qǐng)求時(shí)響應(yīng)code=0,冪等請(qǐng)求時(shí)響應(yīng)code=1,,那這兩次請(qǐng)求得到的就不是“同樣的結(jié)果”了——就跟你沖食堂喊兩次“師傅,,倆包子”之后,師傅先給你倆包子,、再往你臉上扣一碗粥是一回事兒,。 但是,也有很多接口,,除了返回code和msg之外,,還需要返回業(yè)務(wù)數(shù)據(jù)。對(duì)這種接口來(lái)說(shuō),,僅僅返回code顯然就不夠了——否則,,不就等于是你沖食堂窗口喊“師傅,倆包子”,,而師傅只答應(yīng)“來(lái)了”卻不給你包子嗎,? 無(wú)論哪種情況,,核心都是要保證冪等請(qǐng)求和正常請(qǐng)求返回同樣的結(jié)果。 冪等判斷需要控制并發(fā)嗎,?大多數(shù)情況下,,我們都需要通過(guò)根據(jù)請(qǐng)求唯一鍵查詢某個(gè)數(shù)據(jù)來(lái)判斷冪等:如果已有數(shù)據(jù),說(shuō)明是冪等請(qǐng)求,;如果沒(méi)有數(shù)據(jù),,說(shuō)明不是冪等請(qǐng)求。 這個(gè)邏輯基本可行,,除了發(fā)生并發(fā)請(qǐng)求時(shí):此時(shí),,對(duì)并發(fā)進(jìn)來(lái)的兩個(gè)請(qǐng)求來(lái)說(shuō),用請(qǐng)求中的唯一鍵都查不到數(shù)據(jù),,因而他們都會(huì)被判定為冪等請(qǐng)求,。這樣一來(lái),就等同于一筆請(qǐng)求執(zhí)行了兩次,,也就宣告了冪等判斷失敗了,。 所以,如果用這種查詢的方式來(lái)判斷冪等,,請(qǐng)一定要注意控制并發(fā),。 其它還有什么問(wèn)題,大家可以一并提出來(lái)討論,。 常用實(shí)踐利用分布式鎖+緩存做冪等針對(duì)請(qǐng)求中的唯一鍵,,先加分布式鎖,后判斷緩存中是否有值,。如果沒(méi)有值,,則執(zhí)行正常請(qǐng)求,并把正常返回結(jié)果(無(wú)論成功還是失?。┐嫒刖彺?;如果有值,則直接返回緩存結(jié)果,。 這種方式最高效,、最簡(jiǎn)單。但是,,這種方式有兩個(gè)問(wèn)題,。 首先,如果緩存失效了,,那么冪等判斷也就失效了,。當(dāng)然,這種情況比較可控,、概率較小,。 第二,,如果“其它條件不變”這個(gè)前提被打破了,緩存數(shù)據(jù)往往就會(huì)“過(guò)時(shí)”,。此時(shí),,冪等結(jié)果應(yīng)當(dāng)返回“其它條件不變”之前的結(jié)果,還是返回之后的結(jié)果,?這是一個(gè)值得認(rèn)真考慮的問(wèn)題,。 例如,query方法天然冪等,;但這是建立在數(shù)據(jù)庫(kù)數(shù)據(jù)沒(méi)有被更新的前提之下的,。如果query操作有緩存,而且數(shù)據(jù)庫(kù)被更新后沒(méi)有即使更新緩存,,那么query操作應(yīng)該返回緩存里的結(jié)果,?還是應(yīng)該返回?cái)?shù)據(jù)庫(kù)中的實(shí)際結(jié)果? 同理,,如果操作A上有緩存,,它的冪等建立在其基礎(chǔ)數(shù)據(jù)沒(méi)有變更的前提下。然而,,操作B會(huì)更新操作A的基礎(chǔ)數(shù)據(jù),、但不會(huì)更新操作A上的緩存;此時(shí)如果做過(guò)操作A之后,、再做一次操作B,,然后再做操作A,操作A應(yīng)該返回緩存中沒(méi)有被操作B修改過(guò)的結(jié)果,、還是返回?cái)?shù)據(jù)庫(kù)中被操作B修改過(guò)的結(jié)果,? 利用分布式鎖+數(shù)據(jù)庫(kù)做冪等與緩存相比,數(shù)據(jù)庫(kù)是更可靠的持久化工具,。并且,,絕大多數(shù)業(yè)務(wù)操作最終都要把結(jié)果存入數(shù)據(jù)庫(kù)中,。因此,,使用數(shù)據(jù)庫(kù)來(lái)判斷冪等,也比使用緩存更加可靠一些,。 只不過(guò)在這種情況下,,查到數(shù)據(jù)庫(kù)中有數(shù)據(jù)、并判定為冪等之后,,往往還需要我們手動(dòng)把庫(kù)中的業(yè)務(wù)數(shù)據(jù)再組裝為接口返回結(jié)果,。而且,這種方法會(huì)帶來(lái)額外的一次數(shù)據(jù)庫(kù)查詢操作,,如果接口壓力太大,,對(duì)數(shù)據(jù)庫(kù)性能的影響也不可小覷,。 利用數(shù)據(jù)庫(kù)唯一鍵做冪等數(shù)據(jù)庫(kù)的正常增刪改查操作中,只有增是天然不冪等的(“累計(jì)型”的update不算“正?!备牟僮鳎?。但是,我們可以通過(guò)為數(shù)據(jù)庫(kù)表增加唯一索引來(lái)把它轉(zhuǎn)化為冪等操作,。 不過(guò),,使用這種方式時(shí),我們需要捕獲數(shù)據(jù)庫(kù)拋出的唯一鍵沖突異常,,并把這個(gè)異常處理為冪等結(jié)果——有時(shí)也需要再組裝一次返回結(jié)果,。 這種方式不需要做分布式鎖處理,而且比第二種方式少一次數(shù)據(jù)庫(kù)操作,。不過(guò)也需要手動(dòng)轉(zhuǎn)一次結(jié)果,,另外對(duì)數(shù)據(jù)庫(kù)的性能也友好不到哪兒去——唯一索引也會(huì)拖累insert的性能。 其它如果還有其它方式,,不妨提出來(lái)一起討論下,。 |
|