如何保證Redis與MySQL雙寫一致性?連續(xù)兩個面試問到了!
引言
Redis作為一款高效的內(nèi)存數(shù)據(jù)存儲系統(tǒng),憑借其優(yōu)異的讀寫性能和豐富的數(shù)據(jù)結(jié)構(gòu)支持,被廣泛應(yīng)用于緩存層以提升整個系統(tǒng)的響應(yīng)速度和吞吐量。尤其是在與關(guān)系型數(shù)據(jù)庫(如MySQL、PostgreSQL等)結(jié)合使用時,通過將熱點數(shù)據(jù)存儲在Redis中,可以在很大程度上緩解數(shù)據(jù)庫的壓力,提高整體系統(tǒng)的性能表現(xiàn)。
然而,在這種架構(gòu)中,一個不容忽視的問題就是如何確保Redis緩存與數(shù)據(jù)庫之間的雙寫一致性。所謂雙寫一致性,是指當(dāng)數(shù)據(jù)在數(shù)據(jù)庫中發(fā)生變更時,能夠及時且準(zhǔn)確地反映在Redis緩存中,反之亦然,以避免出現(xiàn)因緩存與數(shù)據(jù)庫數(shù)據(jù)不一致導(dǎo)致的業(yè)務(wù)邏輯錯誤或用戶體驗下降。尤其在高并發(fā)場景下,由于網(wǎng)絡(luò)延遲、并發(fā)控制等因素,保證雙寫一致性變得更加復(fù)雜。
在實際業(yè)務(wù)開發(fā)中,若不能妥善處理好緩存與數(shù)據(jù)庫的雙寫一致性問題,可能會帶來諸如數(shù)據(jù)丟失、臟讀、重復(fù)讀等一系列嚴(yán)重影響系統(tǒng)穩(wěn)定性和可靠性的后果。本文將嘗試剖析這一問題,介紹日常開發(fā)中常用的一些策略和模式,并結(jié)合具體場景分析不同的解決方案,為大家提供一些有力的技術(shù)參考和支持。
圖片
談?wù)劮植际较到y(tǒng)中的一致性
分布式系統(tǒng)中的一致性指的是在多個節(jié)點上存儲和處理數(shù)據(jù)時,確保系統(tǒng)中的數(shù)據(jù)在不同節(jié)點之間保持一致的特性。在分布式系統(tǒng)中,一致性通常可以分為以下幾個類別:
1. 強(qiáng)一致性:所有節(jié)點在任何時間都看到相同的數(shù)據(jù)。任何更新操作都會立即對所有節(jié)點可見,保證了數(shù)據(jù)的強(qiáng)一致性。這意味著,如果一個節(jié)點完成了寫操作,那么所有其他節(jié)點讀取相同的數(shù)據(jù)之后,都將看到最新的結(jié)果。強(qiáng)一致性通常需要付出更高的代價,例如增加通信開銷和降低系統(tǒng)的可用性。
2. 弱一致性: 系統(tǒng)中的數(shù)據(jù)在某些情況下可能會出現(xiàn)不一致的狀態(tài),但最終會收斂到一致狀態(tài)。弱一致性下的系統(tǒng)允許在一段時間內(nèi),不同節(jié)點之間看到不同的數(shù)據(jù)狀態(tài)。弱一致性通常用于需要在性能和一致性之間進(jìn)行權(quán)衡的場景,例如緩存系統(tǒng)等。
3. 最終一致性: 是弱一致性的一種特例,它保證了在經(jīng)過一段時間后,系統(tǒng)中的所有節(jié)點最終都會達(dá)到一致狀態(tài)。盡管在數(shù)據(jù)更新時可能會出現(xiàn)一段時間的不一致,但最終數(shù)據(jù)會收斂到一致狀態(tài)。最終一致性通常通過一些技術(shù)手段來實現(xiàn),例如基于版本向量或時間戳的數(shù)據(jù)復(fù)制和同步機(jī)制。
除此之外,還有一些其他的一致性類別,例如:因果一致性,順序一致性,基于本篇文章討論的重點,我們暫且只討論以上三種一致性類別。
什么是雙寫一致性問題?
在分布式系統(tǒng)中,雙寫一致性主要指在一個數(shù)據(jù)同時存在于緩存(如Redis)和持久化存儲(如數(shù)據(jù)庫)的情況下,任何一方的數(shù)據(jù)更新都必須確保另一方數(shù)據(jù)的同步更新,以保持雙方數(shù)據(jù)的一致狀態(tài)。這一問題的核心在于如何在并發(fā)環(huán)境下正確處理緩存與數(shù)據(jù)庫的讀寫交互,防止數(shù)據(jù)出現(xiàn)不一致的情況。
典型場景分析
1. 寫數(shù)據(jù)庫后忘記更新緩存:當(dāng)直接對數(shù)據(jù)庫進(jìn)行更新操作而沒有相應(yīng)地更新緩存時,后續(xù)的讀請求可能仍然從緩存中獲取舊數(shù)據(jù),導(dǎo)致數(shù)據(jù)的不一致。
2. 刪除緩存后數(shù)據(jù)庫更新失?。?nbsp;在某些場景下,為了保證數(shù)據(jù)新鮮度,會在更新數(shù)據(jù)庫前先刪除緩存。但如果數(shù)據(jù)庫更新過程中出現(xiàn)異常導(dǎo)致更新失敗,那么緩存將長時間處于空缺狀態(tài),新的查詢將會直接命中數(shù)據(jù)庫,加重數(shù)據(jù)庫壓力,并可能導(dǎo)致數(shù)據(jù)版本混亂。
3. 并發(fā)環(huán)境下讀寫操作的交錯執(zhí)行:在高并發(fā)場景下,可能存在多個讀寫請求同時操作同一份數(shù)據(jù)的情況。比如,在刪除緩存、寫入數(shù)據(jù)庫的過程中,新的讀請求獲取到了舊的數(shù)據(jù)庫數(shù)據(jù)并放入緩存,此時就出現(xiàn)了數(shù)據(jù)不一致的現(xiàn)象。
4. 主從復(fù)制延遲與緩存失效時間窗口沖突:對于具備主從復(fù)制功能的數(shù)據(jù)庫集群,主庫更新數(shù)據(jù)后,存在一定的延遲才將數(shù)據(jù)同步到從庫。如果在此期間緩存剛好過期并重新從數(shù)據(jù)庫加載數(shù)據(jù),可能會從尚未完成同步的從庫讀取到舊數(shù)據(jù),進(jìn)而導(dǎo)致緩存與主庫數(shù)據(jù)的不一致。
數(shù)據(jù)不一致不僅會導(dǎo)致業(yè)務(wù)邏輯出錯,還可能引發(fā)用戶界面展示錯誤、交易狀態(tài)不準(zhǔn)確等問題,嚴(yán)重時甚至?xí)绊懴到y(tǒng)的正常運(yùn)行和用戶體驗。
解決雙寫一致性問題的主要策略
在解決Redis緩存與數(shù)據(jù)庫雙寫一致性問題上,有多種策略和模式。我們主要介紹以下幾種主要的策略:
Cache Aside Pattern(旁路緩存模式)
Cache Aside Pattern 是一種在分布式系統(tǒng)中廣泛采用的緩存和數(shù)據(jù)庫協(xié)同工作策略,在這個模式中,數(shù)據(jù)以數(shù)據(jù)庫為主存儲,緩存作為提升讀取效率的輔助手段。也是日常中比較常見的一種手段。其工作流程如下:
圖片
由上圖我們可以看出Cache Aside Pattern的工作原理:
? 讀取操作:首先嘗試從緩存中獲取數(shù)據(jù),如果緩存命中,則直接返回;否則,從數(shù)據(jù)庫中讀取數(shù)據(jù)并將其放入緩存,最后返回給客戶端。
? 更新操作:當(dāng)需要更新數(shù)據(jù)時,首先更新數(shù)據(jù)庫,然后再清除或使緩存中的對應(yīng)數(shù)據(jù)失效。這樣一來,后續(xù)的讀請求將無法從緩存獲取數(shù)據(jù),從而迫使系統(tǒng)從數(shù)據(jù)庫加載最新的數(shù)據(jù)并重新填充緩存。
我們從更新操作上看會發(fā)現(xiàn)兩個很有意思的問題:
為什么操作緩存的時候是刪除舊緩存而不是直接更新緩存?
我們舉例模擬下并發(fā)環(huán)境下的更新DB&緩存:
? 線程A先發(fā)起一個寫操作,第一步先更新數(shù)據(jù)庫,然后更新緩存
? 線程B再發(fā)起一個寫操作,第二步更新了數(shù)據(jù)庫,然后更新緩存 當(dāng)以上兩個線程的執(zhí)行,如果嚴(yán)格先后順序執(zhí)行,那么對于更新緩存還是刪除緩存去操作緩存都可以,但是如果兩個線程同時執(zhí)行時,由于網(wǎng)絡(luò)或者其他原因,導(dǎo)致線程B先執(zhí)行完更新緩存,然后線程A才會更新緩存。如下圖:
圖片
這時候緩存中保存的就是線程A的數(shù)據(jù),而數(shù)據(jù)庫中保存的是線程B的數(shù)據(jù)。這時候如果讀取到的緩存就是臟數(shù)據(jù)。但是如果使用刪除緩存取代更新緩存,那么就不會出現(xiàn)這個臟數(shù)據(jù)。這種方式可以簡化并發(fā)控制、保證數(shù)據(jù)一致性、降低操作復(fù)雜度,并能更好地適應(yīng)各種潛在的異常場景和緩存策略。盡管這種方法可能會增加一次數(shù)據(jù)庫訪問的成本,但在實際應(yīng)用中,考慮到數(shù)據(jù)的一致性和系統(tǒng)的健壯性,這是值得付出的折衷。
并且在寫多讀少的情況下,數(shù)據(jù)很多時候并不會被讀取到,但是一直被頻繁的更新,這樣也會浪費(fèi)性能。實際上,寫多的場景,用緩存也不是很劃算。只有在讀多寫少的情況下使用緩存才會發(fā)揮更大的價值。
為什么是先操作數(shù)據(jù)庫再操作緩存?
在操作緩存時,為什么要先操作數(shù)據(jù)庫而不是先操作緩存?我們同樣舉例模擬兩個線程,線程A寫入數(shù)據(jù),先刪除緩存在更新DB,線程B讀取數(shù)據(jù)。流程如下:
1. 線程A發(fā)起一個寫操作,第一步刪除緩存
2. 此時線程B發(fā)起一個讀操作,緩存中沒有,則繼續(xù)讀DB,讀出來一個老數(shù)據(jù)
3. 然后線程B把老數(shù)據(jù)放入緩存中
4. 線程A更新DB數(shù)據(jù)
圖片
所以這樣就會出現(xiàn)緩存中存儲的是舊數(shù)據(jù),而數(shù)據(jù)庫中存儲的是新數(shù)據(jù),這樣就出現(xiàn)臟數(shù)據(jù),所以我們一般都采取先操作數(shù)據(jù)庫,在操作緩存。這樣后續(xù)的讀請求從數(shù)據(jù)庫獲取最新數(shù)據(jù)并重新填充緩存。這樣的設(shè)計降低了數(shù)據(jù)不一致的風(fēng)險,提升了系統(tǒng)的可靠性。同時,這也符合CAP定理中對于一致性(Consistency)和可用性(Availability)權(quán)衡的要求,在很多場景下,數(shù)據(jù)一致性被優(yōu)先考慮。
Cache Aside Pattern相對簡單直觀,容易理解和實現(xiàn)。只需要簡單的判斷和緩存失效邏輯即可,對已有系統(tǒng)的改動較小。并且由于緩存是按需加載的,所以不會浪費(fèi)寶貴的緩存空間存儲未被訪問的數(shù)據(jù),同時我們可以根據(jù)實際情況決定何時加載和清理緩存。
盡管Cache Aside Pattern在大多數(shù)情況下可以保證最終一致性,但它并不能保證強(qiáng)一致性。在數(shù)據(jù)庫更新后的短暫時間內(nèi)(還未開始操作緩存),如果有讀請求發(fā)生,緩存中仍是舊數(shù)據(jù),但是實際數(shù)據(jù)庫中已是最新數(shù)據(jù),造成短暫的數(shù)據(jù)不一致。在并發(fā)環(huán)境下,特別是在更新操作時,有可能在更新數(shù)據(jù)庫和刪除緩存之間的時間窗口內(nèi),新的讀請求加載了舊數(shù)據(jù)到緩存,導(dǎo)致不一致。
Read-Through/Write-Through(讀寫穿透)
Read-Through 和 Write-Through 是兩種與緩存相關(guān)的策略,它們主要用于緩存系統(tǒng)與持久化存儲之間的數(shù)據(jù)交互,旨在確保緩存與底層數(shù)據(jù)存儲的一致性。
Read-Through(讀穿透)
Read-Through 是一種在緩存中找不到數(shù)據(jù)時,自動從持久化存儲中加載數(shù)據(jù)并回填到緩存中的策略。具體執(zhí)行流程如下:
- ? 客戶端發(fā)起讀請求到緩存系統(tǒng)。
- ? 緩存系統(tǒng)檢查是否存在請求的數(shù)據(jù)。
- ? 如果數(shù)據(jù)不在緩存中,緩存系統(tǒng)會透明地向底層數(shù)據(jù)存儲(如數(shù)據(jù)庫)發(fā)起讀請求。
- ? 數(shù)據(jù)庫返回數(shù)據(jù)后,緩存系統(tǒng)將數(shù)據(jù)存儲到緩存中,并將數(shù)據(jù)返回給客戶端。
- ? 下次同樣的讀請求就可以直接從緩存中獲取數(shù)據(jù),提高了讀取效率。
圖片
整體簡要流程類似Cache Aside Pattern,但在緩存未命中的情況下,Read-Through 策略會自動隱式地從數(shù)據(jù)庫加載數(shù)據(jù)并填充到緩存中,而無需應(yīng)用程序顯式地進(jìn)行數(shù)據(jù)庫查詢。
Cache Aside Pattern 更多地依賴于應(yīng)用程序自己來管理緩存與數(shù)據(jù)庫之間的數(shù)據(jù)流動,包括緩存填充、失效和更新。而Read-Through Pattern 則是在緩存系統(tǒng)內(nèi)部實現(xiàn)了一個更加自動化的過程,使得應(yīng)用程序無需關(guān)心數(shù)據(jù)是從緩存還是數(shù)據(jù)庫中獲取,以及如何保持兩者的一致性。在Read-Through 中,緩存系統(tǒng)承擔(dān)了更多的職責(zé),實現(xiàn)了更緊密的緩存與數(shù)據(jù)庫集成,從而簡化了應(yīng)用程序的設(shè)計和實現(xiàn)。
Write-Through(寫穿透)
Write-Through 是一種在緩存中更新數(shù)據(jù)時,同時將更新操作同步到持久化存儲的策略。具體流程如下:
? 當(dāng)客戶端向緩存系統(tǒng)發(fā)出寫請求時,緩存系統(tǒng)首先更新緩存中的數(shù)據(jù)。
? 同時,緩存系統(tǒng)還會把這次更新操作同步到底層數(shù)據(jù)存儲(如數(shù)據(jù)庫)。
? 當(dāng)數(shù)據(jù)在數(shù)據(jù)庫中成功更新后,整個寫操作才算完成。
? 這樣,無論是從緩存還是直接從數(shù)據(jù)庫讀取,都能得到最新一致的數(shù)據(jù)。
圖片
Read-Through 和 Write-Through 的共同目標(biāo)是確保緩存與底層數(shù)據(jù)存儲之間的一致性,并通過自動化的方式隱藏了緩存與持久化存儲之間的交互細(xì)節(jié),簡化了客戶端的處理邏輯。這兩種策略經(jīng)常一起使用,以提供無縫且一致的數(shù)據(jù)訪問體驗,特別適用于那些對數(shù)據(jù)一致性要求較高的應(yīng)用場景。然而,需要注意的是,雖然它們有助于提高數(shù)據(jù)一致性,但在高并發(fā)或網(wǎng)絡(luò)不穩(wěn)定的情況下,仍然需要考慮并發(fā)控制和事務(wù)處理等問題,以防止數(shù)據(jù)不一致的情況發(fā)生。
Write behind (異步緩存寫入)
Write Behind(異步緩存寫入),也稱為 Write Back(回寫)或 異步更新策略,是一種在處理緩存與持久化存儲(如數(shù)據(jù)庫)之間數(shù)據(jù)同步時的策略。在這種模式下,當(dāng)數(shù)據(jù)在緩存中被更新時,并非立即同步更新到數(shù)據(jù)庫,而是將更新操作暫存起來,隨后以異步的方式批量地將緩存中的更改寫入持久化存儲。其流程如下:
? 應(yīng)用程序首先在緩存中執(zhí)行數(shù)據(jù)更新操作,而不是直接更新數(shù)據(jù)庫。
? 緩存系統(tǒng)會將此次更新操作記錄下來,暫存于一個隊列(如日志文件或內(nèi)存隊列)中,而不是立刻同步到數(shù)據(jù)庫。
? 在后臺有一個獨立的進(jìn)程或線程定期(或者當(dāng)隊列積累到一定大小時)從暫存隊列中取出更新操作,然后批量地將這些更改寫入數(shù)據(jù)庫。
圖片
使用 Write Behind 策略時,由于更新并非即時同步到數(shù)據(jù)庫,所以在異步處理完成之前,如果緩存或系統(tǒng)出現(xiàn)故障,可能會丟失部分更新操作。并且對于高度敏感且要求強(qiáng)一致性的數(shù)據(jù),Write Behind 策略并不適用,因為它無法提供嚴(yán)格的事務(wù)性和實時一致性保證。Write Behind 適用于那些可以容忍一定延遲的數(shù)據(jù)一致性場景,通過犧牲一定程度的一致性換取更高的系統(tǒng)性能和擴(kuò)展性。
解決雙寫一致性問題的3種方案
以上我們主要講解了解決雙寫一致性問題的主要策略,但是每種策略都有一定的局限性,所以我們在實際運(yùn)用中,還要結(jié)合一些其他策略去屏蔽上述策略的缺點。
1. 延時雙刪策略
延時雙刪策略主要用于解決在高并發(fā)場景下,由于網(wǎng)絡(luò)延遲、并發(fā)控制等原因造成的數(shù)據(jù)庫與緩存數(shù)據(jù)不一致的問題。
當(dāng)更新數(shù)據(jù)庫時,首先刪除對應(yīng)的緩存項,以確保后續(xù)的讀請求會從數(shù)據(jù)庫加載最新數(shù)據(jù)。但是由于網(wǎng)絡(luò)延遲或其他不確定性因素,刪除緩存與數(shù)據(jù)庫更新之間可能存在時間窗口,導(dǎo)致在這段時間內(nèi)的讀請求從數(shù)據(jù)庫讀取數(shù)據(jù)后寫回緩存,新寫入的緩存數(shù)據(jù)可能還未反映出數(shù)據(jù)庫的最新變更。
所以為了解決這個問題,延時雙刪策略在第一次刪除緩存后,設(shè)定一段短暫的延遲時間,如幾百毫秒,然后在這段延遲時間結(jié)束后再次嘗試刪除緩存。這樣做的目的是確保在數(shù)據(jù)庫更新傳播到所有節(jié)點,并且在緩存中的舊數(shù)據(jù)徹底過期失效之前,第二次刪除操作可以消除緩存中可能存在的舊數(shù)據(jù),從而提高數(shù)據(jù)一致性。
public class DelayDoubleDeleteService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private TaskScheduler taskScheduler;
public void updateAndScheduleDoubleDelete(String key, String value) {
// 更新數(shù)據(jù)庫...
updateDatabase(key, value);
// 刪除緩存
redisTemplate.delete(key);
// 延遲執(zhí)行第二次刪除
taskScheduler.schedule(() -> {
redisTemplate.delete(key);
}, new CronTrigger("0/1 * * * * ?")); // 假設(shè)1秒后執(zhí)行,實際應(yīng)根據(jù)需求設(shè)置定時表達(dá)式
}
// 更新數(shù)據(jù)庫的邏輯
private void updateDatabase(String key, String value) {
}
}
這種方式可以較好地處理網(wǎng)絡(luò)延遲導(dǎo)致的數(shù)據(jù)不一致問題,較少的并發(fā)寫入數(shù)據(jù)庫和緩存,降低系統(tǒng)的壓力。但是,延遲時間的選擇需要權(quán)衡,過短可能導(dǎo)致實際效果不明顯,過長可能影響用戶體驗。并且對于極端并發(fā)場景,仍可能存在數(shù)據(jù)不一致的風(fēng)險。
2. 刪除緩存重試機(jī)制
刪除緩存重試機(jī)制是在刪除緩存操作失敗時,設(shè)定一個重試策略,確保緩存最終能被正確刪除,以維持與數(shù)據(jù)庫的一致性。
在執(zhí)行數(shù)據(jù)庫更新操作后,嘗試刪除關(guān)聯(lián)的緩存項。如果首次刪除緩存失?。ɡ缇W(wǎng)絡(luò)波動、緩存服務(wù)暫時不可用等情況),系統(tǒng)進(jìn)入重試邏輯,按照預(yù)先設(shè)定的策略(如指數(shù)退避、固定間隔重試等)進(jìn)行多次嘗試。直到緩存刪除成功,或者達(dá)到最大重試次數(shù)為止。通過這種方式,即使在異常情況下也能盡量保證緩存與數(shù)據(jù)庫的一致性。
@Service
public class RetryableCacheService {
@Autowired
private CacheManager cacheManager;
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000L))
public void deleteCacheWithRetry(String key) {
((org.springframework.data.redis.cache.RedisCacheManager) cacheManager).getCache("myCache").evict(key);
}
public void updateAndDeleteCache(String key, String value) {
// 更新數(shù)據(jù)庫...
updateDatabase(key, value);
// 嘗試刪除緩存,失敗時自動重試
deleteCacheWithRetry(key);
}
// 更新數(shù)據(jù)庫的邏輯,此處僅示意
private void updateDatabase(String key, String value) {
// ...
}
}
這種重試方式確保緩存刪除操作的成功執(zhí)行,可以應(yīng)對網(wǎng)絡(luò)抖動等導(dǎo)致的臨時性錯誤,提高數(shù)據(jù)一致性。但是可能占用額外的系統(tǒng)資源和時間,重試次數(shù)過多可能會阻塞其他操作。
監(jiān)聽并讀取biglog異步刪除緩存
在數(shù)據(jù)庫發(fā)生寫操作時,將變更記錄在binlog或類似的事務(wù)日志中,然后使用一個專門的異步服務(wù)或者監(jiān)聽器訂閱binlog的變化(比如Canal),一旦檢測到有數(shù)據(jù)更新,便根據(jù)binlog中的操作信息定位到受影響的緩存項。講這些需要更新緩存的數(shù)據(jù)發(fā)送到消息隊列,消費(fèi)者處理消息隊列中的事件,異步地刪除或更新緩存中的對應(yīng)數(shù)據(jù),確保緩存與數(shù)據(jù)庫保持一致。
@Service
public class BinlogEventHandler {
@Autowired
private RocketMQTemplate rocketMQTemplate;
public void handleBinlogEvent(BinlogEvent binlogEvent) {
// 解析binlogEvent,獲取需要更新緩存的key
String cacheKey = deriveCacheKeyFromBinlogEvent(binlogEvent);
// 發(fā)送到RocketMQ
rocketMQTemplate.asyncSend("cacheUpdateTopic", cacheKey, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
// 發(fā)送成功處理
}
@Override
public void onException(Throwable e) {
// 發(fā)送失敗處理
}
});
}
// 從binlog事件中獲取緩存key的邏輯,這里僅為示意
private String deriveCacheKeyFromBinlogEvent(BinlogEvent binlogEvent) {
// ...
}
}
@RocketMQMessageListener(consumerGroup = "myConsumerGroup", topic = "cacheUpdateTopic")
public class CacheUpdateConsumer {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public void onMessage(MessageExt messageExt) {
String cacheKey = new String(messageExt.getBody());
redisTemplate.delete(cacheKey);
}
}
這種方法的好處是將緩存的更新操作與主業(yè)務(wù)流程解耦,避免阻塞主線程,同時還能處理數(shù)據(jù)庫更新后由于網(wǎng)絡(luò)問題或并發(fā)問題導(dǎo)致的緩存更新滯后情況。當(dāng)然,實現(xiàn)這一策略相對復(fù)雜,需要對數(shù)據(jù)庫的binlog機(jī)制有深入理解和定制開發(fā)。
總結(jié)
在分布式系統(tǒng)中,為了保證緩存與數(shù)據(jù)庫雙寫一致性,可以采用以下方案:
1. 讀取操作:
? 先嘗試從緩存讀取數(shù)據(jù),若緩存命中,則直接返回緩存中的數(shù)據(jù)。
? 若緩存未命中,則從數(shù)據(jù)庫讀取數(shù)據(jù),并將數(shù)據(jù)放入緩存。
2. 更新操作:
? 在更新數(shù)據(jù)時,首先在數(shù)據(jù)庫進(jìn)行寫入操作,確保主數(shù)據(jù)庫數(shù)據(jù)的即時更新。
? 為了減少數(shù)據(jù)不一致窗口,采用異步方式處理緩存更新,具體做法是監(jiān)聽數(shù)據(jù)庫的binlog事件,異步進(jìn)行刪除緩存。
? 在一主多從的場景下,為了確保數(shù)據(jù)一致性,需要等待所有從庫的binlog事件都被處理后才刪除緩存(確保全部從庫均已更新)。
同時,還需注意以下要點:
? 對于高并發(fā)環(huán)境,可能需要結(jié)合分布式鎖、消息隊列或緩存失效延時等技術(shù),進(jìn)一步確保并發(fā)寫操作下的數(shù)據(jù)一致性。
? 異步處理binlog時,務(wù)必考慮異常處理機(jī)制和重試策略,確保binlog事件能夠正確處理并執(zhí)行緩存更新操作。