緩存數(shù)據(jù)不一致和并發(fā)競(jìng)爭(zhēng)怎么處理
數(shù)據(jù)不一致
問題描述
同一份數(shù)據(jù),可能會(huì)同時(shí)存在 DB 和緩存之中。那就有可能發(fā)生,DB 和緩存的數(shù)據(jù)不一致。如果緩存有多個(gè)副本,多個(gè)緩存副本里的數(shù)據(jù)也可能會(huì)發(fā)生不一致現(xiàn)象。
原因分析
不一致的問題大多跟緩存更新異常有關(guān)。比如更新 DB 后,寫緩存失敗,從而導(dǎo)致緩存中存的是老數(shù)據(jù)。另外,如果系統(tǒng)采用一致性 Hash 分布,同時(shí)采用 rehash 自動(dòng)漂移策略,在節(jié)點(diǎn)多次上下線之后,也會(huì)產(chǎn)生臟數(shù)據(jù)。緩存有多個(gè)副本時(shí),更新某個(gè)副本失敗,也會(huì)導(dǎo)致這個(gè)副本的數(shù)據(jù)是老數(shù)據(jù)。
業(yè)務(wù)場(chǎng)景
導(dǎo)致數(shù)據(jù)不一致的場(chǎng)景也不少。如下圖所示,在緩存機(jī)器的帶寬被打滿,或者機(jī)房網(wǎng)絡(luò)出現(xiàn)波動(dòng)時(shí),緩存更新失敗,新數(shù)據(jù)沒有寫入緩存,就會(huì)導(dǎo)致緩存和 DB 的數(shù)據(jù)不一致。緩存 rehash 時(shí),某個(gè)緩存機(jī)器反復(fù)異常,多次上下線,更新請(qǐng)求多次 rehash。這樣,一份數(shù)據(jù)存在多個(gè)節(jié)點(diǎn),且每次 rehash 只更新某個(gè)節(jié)點(diǎn),導(dǎo)致一些緩存節(jié)點(diǎn)產(chǎn)生臟數(shù)據(jù)。
解決方案
要盡量保證數(shù)據(jù)的一致性。這里也給出了 3 個(gè)方案,可以根據(jù)實(shí)際情況進(jìn)行選擇。
- 第一個(gè)方案,cache 更新失敗后,可以進(jìn)行重試,如果重試失敗,則將失敗的 key 寫入隊(duì)列機(jī)服務(wù),待緩存訪問恢復(fù)后,將這些 key 從緩存刪除。這些 key 在再次被查詢時(shí),重新從 DB 加載,從而保證數(shù)據(jù)的一致性。
- 第二個(gè)方案,緩存時(shí)間適當(dāng)調(diào)短,讓緩存數(shù)據(jù)及早過(guò)期后,然后從 DB 重新加載,確保數(shù)據(jù)的最終一致性。
- 第三個(gè)方案,不采用 rehash 漂移策略,而采用緩存分層策略,盡量避免臟數(shù)據(jù)產(chǎn)生。
數(shù)據(jù)并發(fā)競(jìng)爭(zhēng)
問題描述
第五個(gè)經(jīng)典問題是數(shù)據(jù)并發(fā)競(jìng)爭(zhēng)。互聯(lián)網(wǎng)系統(tǒng),線上流量較大,緩存訪問中很容易出現(xiàn)數(shù)據(jù)并發(fā)競(jìng)爭(zhēng)的現(xiàn)象。數(shù)據(jù)并發(fā)競(jìng)爭(zhēng),是指在高并發(fā)訪問場(chǎng)景,一旦緩存訪問沒有找到數(shù)據(jù),大量請(qǐng)求就會(huì)并發(fā)查詢 DB,導(dǎo)致 DB 壓力大增的現(xiàn)象。
數(shù)據(jù)并發(fā)競(jìng)爭(zhēng),主要是由于多個(gè)進(jìn)程/線程中,有大量并發(fā)請(qǐng)求獲取相同的數(shù)據(jù),而這個(gè)數(shù)據(jù) key 因?yàn)檎眠^(guò)期、被剔除等各種原因在緩存中不存在,這些進(jìn)程/線程之間沒有任何協(xié)調(diào),然后一起并發(fā)查詢 DB,請(qǐng)求那個(gè)相同的 key,最終導(dǎo)致 DB 壓力大增,如下圖。
業(yè)務(wù)場(chǎng)景
數(shù)據(jù)并發(fā)競(jìng)爭(zhēng)在大流量系統(tǒng)也比較常見,比如車票系統(tǒng),如果某個(gè)火車車次緩存信息過(guò)期,但仍然有大量用戶在查詢?cè)撥嚧涡畔ⅰS直热缥⒉┫到y(tǒng)中,如果某條微博正好被緩存淘汰,但這條微博仍然有大量的轉(zhuǎn)發(fā)、評(píng)論、贊。上述情況都會(huì)造成該車次信息、該條微博存在并發(fā)競(jìng)爭(zhēng)讀取的問題。
解決方案
要解決并發(fā)競(jìng)爭(zhēng),有 2 種方案。
- 方案一是使用全局鎖。如下圖所示,即當(dāng)緩存請(qǐng)求 miss 后,先嘗試加全局鎖,只有加全局鎖成功的線程,才可以到 DB 去加載數(shù)據(jù)。其他進(jìn)程/線程在讀取緩存數(shù)據(jù) miss 時(shí),如果發(fā)現(xiàn)這個(gè) key 有全局鎖,就進(jìn)行等待,待之前的線程將數(shù)據(jù)從 DB 回種到緩存后,再?gòu)木彺娅@取。
- 方案二是,對(duì)緩存數(shù)據(jù)保持多個(gè)備份,即便其中一個(gè)備份中的數(shù)據(jù)過(guò)期或被剔除了,還可以訪問其他備份,從而減少數(shù)據(jù)并發(fā)競(jìng)爭(zhēng)的情況,如下圖。