緩存與數(shù)據(jù)庫(kù)雙寫一致性
這幾天瞎逛,不知道在哪里瞟到了緩存的雙寫,就突然想起來(lái)這塊雖然簡(jiǎn)單,但是細(xì)節(jié)上還是有足夠多我們可以去關(guān)注的點(diǎn)。這篇文章就來(lái)詳細(xì)聊聊雙寫一致性。
首先我們知道,現(xiàn)在將高速緩存應(yīng)用于業(yè)務(wù)當(dāng)中已經(jīng)十分常見(jiàn)了,甚至可能跟數(shù)據(jù)庫(kù)的頻率不相上下。你的用戶量如果上去了,直接將一個(gè)裸的 MySQL 去扛住所有壓力明顯是不合理的。
這里的高速緩存,目前業(yè)界主流的就是 Redis 了,關(guān)于 Redis 相關(guān)的文章,之前也有聊過(guò),在此就不贅述,感興趣的可以看看:
- Redis 基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)和用法
 - Redis 數(shù)據(jù)持久化
 - Redis 主從同步
 - Redis Sentinel 高可用
 - Redis Cluster 集群詳解
 
額,不列出來(lái)我都沒(méi)感覺(jué)關(guān)于 Redis 我居然寫了這么多...言歸正傳。
在我們的業(yè)務(wù)中,普遍都會(huì)需要將一部分常用的熱點(diǎn)數(shù)據(jù)(或者說(shuō)不經(jīng)常變但是又比較多的數(shù)據(jù))放入 Redis 中緩存起來(lái)。下次業(yè)務(wù)來(lái)請(qǐng)求查詢時(shí),就可以直接將 Redis 中的數(shù)據(jù)返回,以此來(lái)減少業(yè)務(wù)系統(tǒng)和數(shù)據(jù)庫(kù)的交互。
這樣有兩個(gè)好處,一個(gè)是能夠降低數(shù)據(jù)庫(kù)的壓力,另一個(gè)自不必說(shuō),對(duì)相同數(shù)據(jù)來(lái)說(shuō)能夠有效的降低 API 的 RT(Response Time)。
后者其實(shí)還好,降低數(shù)據(jù)庫(kù)的壓力顯得尤為重要,因?yàn)槲覀兊臉I(yè)務(wù)服務(wù)雖然能夠以較低的成本做到橫向擴(kuò)展,但數(shù)據(jù)庫(kù)不能。
這里的不能,其實(shí)不是指數(shù)據(jù)庫(kù)不能擴(kuò)展。MySQL 在主從架構(gòu)下,通過(guò)擴(kuò)展 Slave 節(jié)點(diǎn)的數(shù)量可以有效的橫向擴(kuò)展讀請(qǐng)求。而 Master 節(jié)點(diǎn)由于不是無(wú)狀態(tài)的,所以擴(kuò)展起來(lái)很麻煩。
對(duì),是很麻煩,也不是不能橫向擴(kuò)展。但是在那種架構(gòu)下,我舉個(gè)例子,主-主架構(gòu)下,會(huì)帶來(lái)很多意向不到的數(shù)據(jù)同步問(wèn)題,并且對(duì)整個(gè)的架構(gòu)引入了新的復(fù)雜性。
就像我在之前寫的MySQL 主從原理中提到過(guò)的一樣,雙主架構(gòu)更多的意義在于 HA,而不是做負(fù)載均衡。
所以,相同的數(shù)據(jù)會(huì)同時(shí)存在 Redis 和 MySQL 中,如果該數(shù)據(jù)并不會(huì)改變,那就完美的一匹??涩F(xiàn)實(shí)很骨感,這個(gè)數(shù)據(jù)99.9999%的概率是一定會(huì)變的。
為了維護(hù) Redis 和 MySQL 中數(shù)據(jù)的一致性,雙寫的問(wèn)題的就誕生了。
Cache Aside Pattern
其中最經(jīng)典的方案就是 Cache Aside Pattern ,這套定義了一套緩存和數(shù)據(jù)庫(kù)的讀寫方案,以此來(lái)保證緩存和數(shù)據(jù)庫(kù)中的數(shù)據(jù)一致性。
具體方案
Cache Aside Pattern 具體又分為兩種 Case,分別是讀和寫。
對(duì)于讀請(qǐng)求,會(huì)先去 Redis 中查詢數(shù)據(jù),如果命中了就會(huì)直接返回?cái)?shù)據(jù)。而如果沒(méi)有從緩存中獲取到,就會(huì)去 DB 中查詢,將查詢到的數(shù)據(jù)寫回 Redis,然后返回響應(yīng)。
而更新則相對(duì)簡(jiǎn)單,但是也是最具有爭(zhēng)議。當(dāng)收到寫請(qǐng)求時(shí),會(huì)先更新 DB 中的數(shù)據(jù),成功之后再將緩存中的數(shù)據(jù)刪除。
注意這里是刪除,而不是更新。因?yàn)閷?shí)際生產(chǎn)中,緩存中存放的可能不僅僅是單一的像 true、false或者1、19這種值。
為什么是刪除
還有可能在緩存中存放一整個(gè)結(jié)構(gòu)體,其中包含了非常多的字段。那么是不是每次有一個(gè)字段更新就都需要去把數(shù)據(jù)從緩存中讀取出來(lái),解析成對(duì)應(yīng)的結(jié)構(gòu)體,然后更新對(duì)應(yīng)字段的值,再寫回緩存呢?又或者你是直接將原緩存刪除,然后又將最新的數(shù)據(jù)寫入緩存?
其實(shí)乍一看,好像沒(méi)有毛病。我更新難道不應(yīng)該這么更新嗎?在這里,我們的關(guān)注點(diǎn)更多的放在了更新的方式上,而把更多的必要性給忽略到了。我們更新了這個(gè)值之后,在接下來(lái)的一段時(shí)間內(nèi),它會(huì)被頻繁訪問(wèn)到嗎?可能會(huì),但也可能根本不會(huì)被訪問(wèn)到了。
那既然有可能不會(huì)被訪問(wèn)到, 那我們?yōu)樯哆€要去更新它?而且,更新緩存所帶來(lái)的開(kāi)銷有時(shí)侯會(huì)非常大。
然而這還只是緩存數(shù)據(jù)源單一的情況,如果緩存中緩存的是某個(gè)讀模型,其數(shù)據(jù)是通過(guò)多張表的數(shù)據(jù)計(jì)算得出的,其開(kāi)銷會(huì)更大。
讀模型,簡(jiǎn)單理解就是用現(xiàn)有數(shù)據(jù),計(jì)算、統(tǒng)計(jì)出來(lái)的一些數(shù)據(jù)。
這個(gè)思路就類似于懶加載的方式,只在需要的時(shí)候去計(jì)算它。
爭(zhēng)議在哪兒?
前面提到過(guò),更新時(shí)順序?yàn)橄雀?DB 中的數(shù)據(jù),成功之后再刪除緩存。但是也有人認(rèn)為應(yīng)該先刪除緩存,再去更新 DB 中的數(shù)據(jù)。
乍一看,可能并不能發(fā)現(xiàn)問(wèn)題。甚至覺(jué)得還有那么一絲絲合理。因?yàn)槿绻葎h除緩存,如果刪除操作失敗,DB 中的數(shù)據(jù)也不會(huì)更新,這樣緩存和 DB 中數(shù)據(jù)也能保證一致性。而且,如果刪除緩存成功,但更新 DB 失敗了,大不了下次獲取時(shí),再將數(shù)據(jù)寫回緩存即可,可以說(shuō)十分的合理。
但,這只是單線程的情況下,如果在多線程下,會(huì)直接造成致命的數(shù)據(jù)不一致。
上面的流程圖詳細(xì)的描述了情況,更新請(qǐng)求1剛剛把緩存中的數(shù)據(jù)刪除,查詢請(qǐng)求2就過(guò)來(lái)了,查詢請(qǐng)求2會(huì)發(fā)現(xiàn)緩存中是空的,所以按照 Cache Aside Pattern 的讀請(qǐng)求標(biāo)準(zhǔn),會(huì)從 DB 中加載最新的數(shù)據(jù)并將其寫入緩存。而此時(shí)更新請(qǐng)求1還沒(méi)有對(duì) DB 進(jìn)行更新操作,所以查詢請(qǐng)求2寫入到緩存中的數(shù)據(jù)仍然是舊數(shù)據(jù)。
這樣一來(lái),查詢請(qǐng)求3在下一次更新之前,讀取到的就都會(huì)是老數(shù)據(jù)。然后,更新請(qǐng)求1將最新的數(shù)據(jù)更新至 DB,緩存和 DB 的數(shù)據(jù)就不一致了。
其實(shí) Cache Aside Pattern 中的模式,仍然會(huì)在某些 case 下造成數(shù)據(jù)不一致。但是這個(gè)概率非常的低,因?yàn)橛|發(fā)這個(gè)不一致的情況的條件太苛刻了。
首先是緩存要失效,然后讀請(qǐng)求、寫請(qǐng)求并發(fā)的執(zhí)行,并且讀請(qǐng)求要比寫請(qǐng)求后執(zhí)行完。為啥說(shuō)概率不大呢,首先在實(shí)際生產(chǎn)中,讀請(qǐng)求一般都要比寫請(qǐng)求快得多。除此之外,讀請(qǐng)求去 DB 請(qǐng)求數(shù)據(jù)的時(shí)間一定要早于寫請(qǐng)求,并且寫緩存的時(shí)間還要一定晚于寫請(qǐng)求,比起最開(kāi)始的那種情況來(lái)說(shuō),條件已經(jīng)是非常的嚴(yán)格了。
如果完全不能容忍,可以通過(guò) 2PC 的模式去保證數(shù)據(jù)的一致性,也可以通過(guò)將請(qǐng)求串行化的方式來(lái)解決,但這樣的代價(jià)就是會(huì)犧牲并發(fā)量。
End
其實(shí)還有其他的幾種方案,比如 Read Throught Pattern 、Write Through Pattern、Write Around、Write Behind Caching Pattern 等等。但是這些相對(duì)于 Cache Aside Pattern 來(lái)說(shuō)比較簡(jiǎn)單,可以自己去了解一下就好。




















 
 
 

















 
 
 
 