最近工作中江掩,在解決配送核心系統(tǒng)的一個(gè)熱點(diǎn)問(wèn)題。為了問(wèn)題解決核畴,需要對(duì)核心業(yè)務(wù)數(shù)據(jù)庫(kù)進(jìn)行散表/拆表優(yōu)化膝但。過(guò)程中,發(fā)現(xiàn)大家對(duì)一致性問(wèn)題的認(rèn)知不完全對(duì)齊膛檀,所以想著還是有必要整理歸納一下分布式系統(tǒng)環(huán)境中一致性問(wèn)題出現(xiàn)的場(chǎng)景锰镀、原因、一致性問(wèn)題的定義及對(duì)應(yīng)方案(或者說(shuō)‘套路’ --- 最近重新翻看《萬(wàn)萬(wàn)沒(méi)想到 - 用理工科思維理解世界》咖刃,其中就講到了要‘掌握套路’,通過(guò)不斷‘練習(xí)’憾筏,在頭腦中形成針對(duì)問(wèn)題及技能的‘神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)’ 嚎杨。當(dāng)碰到類似場(chǎng)景或者問(wèn)題時(shí),能夠一眼識(shí)別出‘問(wèn)題’模式氧腰、并采取對(duì)應(yīng)方案來(lái)解決)枫浙。
一致性問(wèn)題的場(chǎng)景
先來(lái)看看幾個(gè)一致性問(wèn)題的場(chǎng)景。
場(chǎng)景1 - 一個(gè)用戶通過(guò)web 服務(wù)上傳一個(gè)全尺寸圖片并保存到一個(gè)文件存儲(chǔ)當(dāng)中古拴。而一般文件存儲(chǔ)為提供數(shù)據(jù)可靠性箩帚,往往都會(huì)對(duì)上傳的內(nèi)容進(jìn)行備份。另外一個(gè)應(yīng)用-‘圖片調(diào)整模塊’通過(guò)監(jiān)聽(tīng)消息隊(duì)列中’圖片上傳‘事件黄痪,會(huì)從文件存儲(chǔ)當(dāng)中讀取文件紧帕,并進(jìn)行圖片尺寸調(diào)整,然后將調(diào)整后的圖片再保存回文件存儲(chǔ)中桅打。
這個(gè)場(chǎng)景下是嗜,可能的一致性問(wèn)題是:‘圖片調(diào)整模塊’收到有文件發(fā)布消息后,就會(huì)從文件存儲(chǔ)中嘗試讀取該消息中對(duì)應(yīng)地址的圖片挺尾。而由于文件存儲(chǔ)是分布式的多副本系統(tǒng)鹅搪,有可能會(huì)返回這個(gè)圖片的一個(gè)舊的副本,然后開(kāi)始進(jìn)行圖片調(diào)整操作遭铺,并將調(diào)整完后的圖片寫回保存丽柿。這樣一來(lái)恢准,就會(huì)使得真正需要進(jìn)行調(diào)整的新圖片被覆蓋。
相信這類場(chǎng)景甫题,在其他日常的業(yè)務(wù)/系統(tǒng)需求中馁筐,很常見(jiàn)。如上游服務(wù)創(chuàng)建了一個(gè)訂單寫入到數(shù)據(jù)庫(kù)中幔睬,然后通過(guò)MQ消息通知下游服務(wù)眯漩,并通過(guò)RPC接口來(lái)調(diào)用獲取訂單信息(一般的數(shù)據(jù)庫(kù)都采用主從集群,而服務(wù)端如不加以區(qū)分處理麻顶,則有可能讀從庫(kù)----數(shù)據(jù)延遲赦抖,而讀到舊數(shù)據(jù)),下游服務(wù)獲得后修改辅肾,然后再寫回队萤。這一類場(chǎng)景中,如果不關(guān)注一致性相關(guān)問(wèn)題矫钓,可能就會(huì)出現(xiàn)數(shù)據(jù)更新丟失的業(yè)務(wù)錯(cuò)誤要尔。
場(chǎng)景2 - 一個(gè)專門用于對(duì)用戶進(jìn)行運(yùn)營(yíng)獎(jiǎng)勵(lì)的業(yè)務(wù)服務(wù)A,基于產(chǎn)品同學(xué)新的產(chǎn)品需求---- “對(duì)在某個(gè)時(shí)間范圍內(nèi)購(gòu)買了某些商品且訂單金額大于某個(gè)下限值的用戶進(jìn)行發(fā)券獎(jiǎng)勵(lì)”新娜,RD基于這個(gè)需求開(kāi)發(fā)赵辕、上線。但卻忽略了商品訂單可能會(huì)被用戶退貨/申請(qǐng)退款(而這個(gè)業(yè)務(wù)場(chǎng)景很可能并不會(huì)被負(fù)責(zé)用戶運(yùn)營(yíng)的研發(fā)RD和產(chǎn)品同學(xué)知道)的場(chǎng)景概龄。那么还惠,一旦這樣的情況發(fā)生,業(yè)務(wù)服務(wù)A之前發(fā)送給用戶的獎(jiǎng)勵(lì)優(yōu)惠券的條件就相當(dāng)于失效了私杜,但優(yōu)惠券卻已經(jīng)發(fā)送了出去蚕键。更有甚者,如果被黑產(chǎn)盯上衰粹,撞到了這個(gè)漏洞锣光,就可能引發(fā)業(yè)務(wù)安全事件,惡意刷券铝耻。
這個(gè)不一致的場(chǎng)景誊爹,雖然不會(huì)造成數(shù)據(jù)上的丟失,但卻造成了業(yè)務(wù)錯(cuò)誤田篇,而且這類錯(cuò)誤往往在業(yè)務(wù)復(fù)雜的系統(tǒng)中會(huì)出現(xiàn)替废,尤其產(chǎn)品或研發(fā)負(fù)責(zé)不同的業(yè)務(wù)服務(wù)/產(chǎn)品方向,一個(gè)新的產(chǎn)品需求依賴某個(gè)對(duì)象數(shù)據(jù)而又沒(méi)有考慮之前’歷史悠久‘的’會(huì)改變這個(gè)對(duì)象數(shù)據(jù)‘的場(chǎng)景泊柬,就會(huì)出現(xiàn)這種狀態(tài)不一致的問(wèn)題椎镣。
場(chǎng)景3 - 與場(chǎng)景2 類似,但引起問(wèn)題的原因不同兽赁。 相信大家都用過(guò)Redis的分布式鎖状答,來(lái)對(duì)某些競(jìng)爭(zhēng)資源/數(shù)據(jù)狀態(tài)進(jìn)行加鎖控制冷守,以避免多服務(wù)/多線程并發(fā)進(jìn)行修改,且一個(gè)分布式鎖為了最終能被釋放惊科,除了在程序中通過(guò)在finally 代碼塊兒中顯示進(jìn)行鎖的釋放之外拍摇,都會(huì)加一個(gè)系統(tǒng)自動(dòng)過(guò)期時(shí)間,且一般較長(zhǎng)(如10 ~ 20 秒)馆截,以防止鎖不能被釋放充活、資源/數(shù)據(jù)狀態(tài)一直被占用。這樣的一個(gè)場(chǎng)景會(huì)有什么一致性的問(wèn)題呢蜡娶?
本場(chǎng)景中混卵,正常情況下不會(huì)有什么問(wèn)題。但由于分布式系統(tǒng)的不確定性窖张,就會(huì)導(dǎo)致系統(tǒng)出現(xiàn)’意料之外‘的問(wèn)題 ---- 由于系統(tǒng)依賴外部數(shù)據(jù)異常(如幕随,所需要處理的數(shù)據(jù)量突然變多)或系統(tǒng)bug,導(dǎo)致該服務(wù)GC時(shí)間特別長(zhǎng)(如前段時(shí)間親身經(jīng)歷過(guò)的線上核心服務(wù)就發(fā)生了一次超過(guò)1分鐘的GC)宿接,還沒(méi)有等到服務(wù)應(yīng)用執(zhí)行finally 代碼進(jìn)行鎖釋放赘淮,Redis 系統(tǒng)就自動(dòng)觸發(fā)了鎖過(guò)期釋放,而此時(shí)另外一個(gè)服務(wù)實(shí)例開(kāi)始搶占相同的這把’鎖‘睦霎,并開(kāi)始執(zhí)行其處理邏輯梢卸。但也就在此時(shí),從長(zhǎng)時(shí)間GC恢復(fù)過(guò)來(lái)的線程開(kāi)始執(zhí)行finally 代碼塊兒中鎖釋放操作副女,就會(huì)把剛剛被另外一個(gè)線程搶占的鎖釋放掉低剔,造成系統(tǒng)和業(yè)務(wù)錯(cuò)誤。
場(chǎng)景4 - 兩個(gè)用戶先后對(duì)數(shù)據(jù)庫(kù)的counter 字段進(jìn)行加一操作肮塞。方式是進(jìn)行‘read-modify-write’操作,由于時(shí)間差姻锁,用戶2在讀取counter 數(shù)據(jù)后進(jìn)行加一操作(而此時(shí)用戶1先于用戶2進(jìn)行了數(shù)據(jù)庫(kù)更新)枕赵,然后再寫回。這樣一來(lái)導(dǎo)致counter并沒(méi)有依次進(jìn)行累加位隶,而是覆蓋拷窜,導(dǎo)致數(shù)據(jù)丟失。
場(chǎng)景5 - Alice在銀行有1000美元的存款涧黄,分為兩個(gè)賬戶篮昧,每個(gè)500美元。現(xiàn)在有這樣一筆轉(zhuǎn)賬交易從其賬戶 1 轉(zhuǎn)100美元到賬戶2 笋妥。如果在她提交轉(zhuǎn)賬請(qǐng)求之后而銀行數(shù)據(jù)庫(kù)系統(tǒng)執(zhí)行轉(zhuǎn)賬的過(guò)程中間懊昨,來(lái)查看兩個(gè)賬戶的余額,她有可能會(huì)看到賬號(hào)2在收到轉(zhuǎn)賬之前的余額(500 美元)和賬戶1在完成轉(zhuǎn)賬之后的余額(400 美元)春宣。對(duì)于 Alice來(lái)說(shuō)酵颁,貌似她的賬戶總共只有900美元嫉你,有100美元消失了。
場(chǎng)景6 - 對(duì)于由數(shù)據(jù)庫(kù)和緩存組成的訂單系統(tǒng)(數(shù)據(jù)庫(kù)用于存儲(chǔ)訂單全量數(shù)據(jù)躏惋,緩存用于存儲(chǔ)熱點(diǎn)數(shù)據(jù))幽污,訂單每次更新,先更新數(shù)據(jù)庫(kù)再更新緩存簿姨,若緩存更新失敗則進(jìn)行重試距误。在這樣的架構(gòu)下,若發(fā)生前后兩次對(duì)訂單標(biāo)簽字段(一般數(shù)據(jù)庫(kù)中都會(huì)設(shè)計(jì)一個(gè)以json 格式存放各類擴(kuò)展信息的字段)的更新扁位,由于網(wǎng)絡(luò)抖動(dòng)致使第一次更新失敗准潭,而第二次更新成功,但第一次緩存更新在經(jīng)過(guò)重試一次后贤牛,成功寫入緩存惋鹅,但卻也覆蓋了第二次更新的數(shù)據(jù),造成數(shù)據(jù)丟失殉簸。
一致性問(wèn)題出現(xiàn)的原因
綜合上述場(chǎng)景闰集,我們可以從中歸納發(fā)生不一致性問(wèn)題的規(guī)律和原因:
(1)數(shù)據(jù)狀態(tài)由一個(gè)操作促使其發(fā)生改變,而另外一個(gè)操作讀到的數(shù)據(jù)是舊的狀態(tài)般卑,并基于舊數(shù)據(jù)進(jìn)行修改武鲁。而在這個(gè)過(guò)程中,讓另外一個(gè)操作讀取舊數(shù)據(jù)狀態(tài)的原因又可以演變成常見(jiàn)的如下幾種:
? ? 場(chǎng)景1 - 一個(gè)操作更新完成后蝠检,通過(guò)另外一個(gè)通道告知下一個(gè)操作去讀數(shù)據(jù)并操作沐鼠,而所讀數(shù)據(jù)是一個(gè)存儲(chǔ)中的舊的副本(并非原有被真正改寫的數(shù)據(jù)),由于多副本間數(shù)據(jù)更新延遲叹谁,導(dǎo)致讀取到舊數(shù)據(jù)饲梭。因此后續(xù)的操作會(huì)依據(jù)舊數(shù)據(jù)進(jìn)行修改而導(dǎo)致不一致。
? ? 場(chǎng)景2 - 一個(gè)操作讀到數(shù)據(jù)后并據(jù)此進(jìn)行后續(xù)處理焰檩,但另外一個(gè)操作改變了原有數(shù)據(jù)狀態(tài)憔涉,使得之前的操作不再符合其邏輯約束。這里依賴同一份數(shù)據(jù)的不同操作分屬于不同的業(yè)務(wù)系統(tǒng)析苫,甚至不同的時(shí)間周期兜叨,難以在系統(tǒng)間通過(guò)‘并發(fā)鎖’等方式進(jìn)行有效控制。
? ? 場(chǎng)景3 - 一個(gè)操作通過(guò)原子API獲得了鎖衩侥,但是被另外一個(gè)操作釋放国旷,數(shù)據(jù)狀態(tài)發(fā)生了改變。而第一個(gè)操作由于系統(tǒng)異常茫死,發(fā)生了意料之外的長(zhǎng)時(shí)間GC跪但,超過(guò)了預(yù)期內(nèi)釋放鎖的時(shí)間,執(zhí)行順序錯(cuò)亂璧榄,使得其從故障中恢復(fù)后再對(duì)其操作時(shí)特漩,已不是之前的那個(gè)鎖吧雹。
? ? 場(chǎng)景4 - 兩個(gè)并發(fā)的針對(duì)同一份數(shù)據(jù)的‘read-modify-write’操作是非原子性操作,使得兩個(gè)操作讀到了相同的數(shù)據(jù)并發(fā)進(jìn)行了修改涂身,從而數(shù)據(jù)被覆蓋而丟失雄卷。
(2)數(shù)據(jù)的正確狀態(tài)由多個(gè)對(duì)象(與之對(duì)應(yīng)的是單體對(duì)象/一份數(shù)據(jù))的完整性約束關(guān)系確定。而針對(duì)多個(gè)對(duì)象的多個(gè)多步操作并不能進(jìn)行很好的隔離(如數(shù)據(jù)庫(kù)的事務(wù)隔離級(jí)別)蛤售,從而導(dǎo)致整體上數(shù)據(jù)不一致丁鹉,如:
? ? 場(chǎng)景5 - 一個(gè)讀事務(wù)(分別讀取同一個(gè)用戶的兩個(gè)賬號(hào)),一個(gè)更新事務(wù)(先給賬號(hào)1加100悴能,再給賬號(hào)2減100)揣钦,每個(gè)事務(wù)都有兩步操作。但由于缺乏了類似‘快照隔離’的事務(wù)隔離機(jī)制漠酿,在讀事務(wù)中嘗試讀取第二個(gè)賬號(hào)時(shí)冯凹,讀到了晚于讀事務(wù)創(chuàng)建的更新事務(wù)提交后的數(shù)據(jù),而非快照數(shù)據(jù)炒嘲,使得數(shù)據(jù)整體上變成了900宇姚,導(dǎo)致‘不可重復(fù)讀’
? ? (注:數(shù)據(jù)庫(kù)事務(wù)隔離級(jí)別(Isolation)是數(shù)據(jù)庫(kù)事務(wù)4個(gè)屬性(特性、ACID)中的一個(gè)夫凸,主要用于保障當(dāng)有多個(gè)并發(fā)事務(wù)發(fā)生時(shí)避免竟態(tài)條件的出現(xiàn)使得數(shù)據(jù)錯(cuò)誤浑劳。而對(duì)于一致性的保障,更多的是由應(yīng)用程序通過(guò)借助數(shù)據(jù)庫(kù)的事務(wù)原子性和事務(wù)隔離型夭拌,來(lái)達(dá)到數(shù)據(jù)狀態(tài)一致性魔熏。一致性問(wèn)題不源于數(shù)據(jù)庫(kù)而是應(yīng)用程序的“預(yù)期狀態(tài)”。因此鸽扁,不同的數(shù)據(jù)庫(kù)事務(wù)級(jí)別也只能解決與其對(duì)應(yīng)的不同層次的數(shù)據(jù)一致性問(wèn)題蒜绽。如場(chǎng)景5中的‘不可重復(fù)讀’問(wèn)題可以通過(guò)設(shè)置‘快照隔離級(jí)別’來(lái)解決,而‘臟讀’和‘臟寫’問(wèn)題可以通過(guò)‘讀-提交’隔離級(jí)別來(lái)解決桶现。這里就不對(duì)‘臟讀’和‘臟寫’的場(chǎng)景進(jìn)行贅述了滓窍,其本質(zhì)上與該原因(1)解釋的‘讀到舊數(shù)據(jù)’類似,它是讀到了事務(wù)過(guò)程中的‘臨時(shí)數(shù)據(jù)’并進(jìn)行了修改)
(3)數(shù)據(jù)狀態(tài)本應(yīng)由多個(gè)操作按照邏輯正確的順序進(jìn)行改變以達(dá)到最終正確的狀態(tài)巩那,但執(zhí)行時(shí)由于異步等原因,使得順序不再一致此蜈,從而導(dǎo)致?tīng)顟B(tài)不一致即横,如場(chǎng)景6
在列舉了上述典型不一致場(chǎng)景及產(chǎn)生的原因之后,我們其實(shí)可以進(jìn)一步地總結(jié)分布式系統(tǒng)下一致性問(wèn)題產(chǎn)生的‘套路’了裆赵,并在碰到如下類似模式/場(chǎng)景的時(shí)候东囚,加以小心檢查與分析:
- 單體對(duì)象的多副本更新延遲,讀寫并發(fā)
- 針對(duì)同一數(shù)據(jù)都有依賴战授,但卻在不同業(yè)務(wù)場(chǎng)景的跨領(lǐng)域業(yè)務(wù)系統(tǒng)有變更
- 網(wǎng)絡(luò)抖動(dòng)页藻、系統(tǒng)異常假死等因素桨嫁,導(dǎo)致針對(duì)同一數(shù)據(jù)(多數(shù)據(jù))的多操作執(zhí)行順序錯(cuò)亂
- 多對(duì)象的跨業(yè)務(wù)服務(wù)/跨數(shù)據(jù)庫(kù)的操作,由于異步操作而導(dǎo)致順序錯(cuò)亂
本文先整理歸納一致性問(wèn)題出現(xiàn)的場(chǎng)景及原因份帐,下一篇?jiǎng)t重點(diǎn)在分布式系統(tǒng)中一致性問(wèn)題的定義和解決方案層面璃吧。