蝦皮一面:如何保證數(shù)據(jù)雙寫一致?
年關(guān)將至,又到了準備面試跳槽的季節(jié)了。據(jù)不完全統(tǒng)計,跳槽是漲薪最快的方式,沒有之一。而跳槽成功與否的關(guān)鍵是“面試”,所以認真準備面試 = 快速漲薪。
準備面試,自然就少不了刷面試真題了,而今天這份剛出爐的蝦皮 Java 后端面試題就非常典型,它的難度適中,面試結(jié)構(gòu)分為:半小時八股 + 半小時手撕代碼,是目前典型的大公司的面試方式,真題如下:
面試問題很多,一篇文章肯定是寫不完的(大部分面試題可以在我的網(wǎng)站上找到答案:www.javacn.site),咱們今天就拿里面最經(jīng)典,最常見的面試題來聊一聊:如何保證緩存和數(shù)據(jù)庫的雙寫一致性?
1、什么是雙寫一致性?
在分布式系統(tǒng)中,數(shù)據(jù)庫和緩存會搭配一起使用,以此來保證程序的整體查詢性能。
也就說,分布式系統(tǒng)為了緩解數(shù)據(jù)庫查詢的壓力,會將查出來的數(shù)據(jù)保存在緩存中,下次再查詢時,直接走緩存系統(tǒng),而不再查詢數(shù)據(jù)庫,這樣就極大的提高了整體的查詢性能。
(1)為什么緩存比數(shù)據(jù)庫快?
緩存之所以比數(shù)據(jù)庫快的主要原因有以下 3 點:
- 內(nèi)存訪問速度快:緩存通常將數(shù)據(jù)存儲在內(nèi)存中,而數(shù)據(jù)庫將數(shù)據(jù)存儲在磁盤上。相比于磁盤訪問,內(nèi)存訪問速度更快,可以達到納秒級別的讀取速度,遠遠快于數(shù)據(jù)庫的毫秒級別的讀取速度。
- IO 操作次數(shù)少:數(shù)據(jù)庫通常需要進行磁盤 IO 操作,包括讀取和寫入磁盤數(shù)據(jù)。而緩存將數(shù)據(jù)存儲在內(nèi)存中,避免了磁盤 IO 的開銷。內(nèi)存訪問不需要進行磁盤尋址和機械運動,相對來說速度更快。
- 特殊的數(shù)據(jù)結(jié)構(gòu):緩存的數(shù)據(jù)結(jié)構(gòu)通常為 key-value 形式的,也就是說緩存可以做到任何數(shù)據(jù)量級下的查詢數(shù)據(jù)復(fù)雜度為 O(1),所以它的查詢效率是非常高的;而數(shù)據(jù)庫采用的是傳統(tǒng)數(shù)據(jù)結(jié)構(gòu)設(shè)計,可能需要查詢二叉樹、或全文搜索、或回表查詢等操作,所以其查詢性能是遠低于緩存系統(tǒng)的。
(2)緩存一致性問題
雖然緩存可以極大的提高查詢性能,但同時也帶來的新的問題:數(shù)據(jù)庫和緩存一致性的問題。
具體來說,在一個常見的應(yīng)用場景中,當更新數(shù)據(jù)庫的操作完成后,需要同步更新緩存,以保證緩存中的數(shù)據(jù)與數(shù)據(jù)庫中的數(shù)據(jù)保持一致。然而,由于數(shù)據(jù)庫和緩存是兩個不同的組件,它們的數(shù)據(jù)更新操作是異步的,可能存在以下問題:
- 數(shù)據(jù)延遲:數(shù)據(jù)庫更新和緩存更新之間存在時間延遲,導(dǎo)致緩存中的數(shù)據(jù)不是最新的。這可能會引起數(shù)據(jù)的不一致,當其他請求讀取數(shù)據(jù)時,可能會讀取到舊的數(shù)據(jù)。
- 更新失敗:在嘗試更新緩存時,可能出現(xiàn)更新失敗的情況。例如,緩存節(jié)點暫時不可用,網(wǎng)絡(luò)故障等。如果更新緩存失敗而未進行適當?shù)奶幚?,也會?dǎo)致數(shù)據(jù)庫和緩存之間的數(shù)據(jù)不一致。
也就說,因為以上原因,可能會導(dǎo)致 A 用戶和 B 用戶執(zhí)行了同一個查詢操作,但是得到了完全不同的結(jié)果,這就是數(shù)據(jù)庫和緩存的一致性問題。
2、如何解決一致性問題?
解決緩存和數(shù)據(jù)庫一致問題的常見解決方案有以下 4 種:
- 先修改數(shù)據(jù)庫,后更新緩存。
- 先更新緩存,后修改數(shù)據(jù)庫。
- 先修改數(shù)據(jù)庫,后刪除緩存。
- 先刪除緩存,后修改數(shù)據(jù)庫。
然而,前 3 種解決方案,有同一個問題,也就是當?shù)谝徊讲僮鲌?zhí)行完之后,第二步未執(zhí)行的情況下,就會導(dǎo)致數(shù)據(jù)庫和緩存的一致性問題,例如第一步執(zhí)行完之后,系統(tǒng)掉電了,那么一致性問題就會一直存在。
相比之下,第 4 種解決方案(先刪除緩存,后修改數(shù)據(jù)庫)相比于前三種解決方案更有優(yōu)勢,起碼它保證了雙方都未執(zhí)行成功,那么從數(shù)據(jù)一致性層面來講,第 4 種方案起碼保證了一定的數(shù)據(jù)一致性,然而第 4 種執(zhí)行方案依然存在其他問題,例如以下這幾個:
- 業(yè)務(wù)完整性問題:程序只執(zhí)行了一半,第一步執(zhí)行完了但第二步未執(zhí)行的情況。
- 并發(fā)保存舊值的問題:在并發(fā)環(huán)境下,第四種方案可能會導(dǎo)致緩存保存舊值的情況,例如以下執(zhí)行情況:
3、最終解決方案
所以,想要最大程度的雙寫一致性問題的最終解決方案是:消息隊列 + 延遲雙刪策略。
(1)為什么要使用消息隊列?
因為消息隊列里面有消息確認機制,它可以保證我們執(zhí)行完第一步之后,即時掉電重啟的情況,依然可以執(zhí)行后續(xù)的流程,因為之前的消息,未進行消息確認,所以程序重啟之后,會繼續(xù)執(zhí)行后續(xù)的流程,這樣就保證了業(yè)務(wù)執(zhí)行的完整性。
(2)什么是延遲雙刪?
延遲雙刪指的是刪除兩次緩存(并且最后一次是延遲刪除),具體執(zhí)行流程如下:
- 刪除緩存
- 更新數(shù)據(jù)庫
- 延遲一會再刪除緩存
最后一次延遲刪除緩存的原因是,為了避免上面因為并發(fā)問題導(dǎo)致保存舊值的情況發(fā)生,所以會延遲一段時間之后再進行刪除操作。這樣即使有并發(fā)問題,也能最大限度的解決保存舊值的情況,因為是延遲之后刪除的,所以即使因為并發(fā)問題保存了舊值,但延遲一段時間之后舊值就會被刪除,那么這樣就自然而然的保證了數(shù)據(jù)庫和緩存的最終一致性。
小結(jié)
數(shù)據(jù)庫和緩存雙寫一致性問題是一道經(jīng)典的面試題,最初解決方案是先更新數(shù)據(jù)庫、再刪除緩存,然而如果發(fā)生掉電情況,只執(zhí)行了前一步操作,那么緩存和數(shù)據(jù)庫就出現(xiàn)了不一致性的問題。為了解決這個問題,所以通常會采用延遲雙刪 + 消息隊列來保證業(yè)務(wù)的完整執(zhí)行和數(shù)據(jù)一致性問題。