緩存更新策略 - 一致性

[TOC]

參考

Cache Aside Pattern

究竟先操作緩存,還是數(shù)據(jù)庫(kù)?

緩存更新的套路

使用緩存的正確姿勢(shì)

緩存的正確使用方式,你都會(huì)了嗎?

1. 概要

緩存服務(wù)和數(shù)據(jù)服務(wù)(如數(shù)據(jù)庫(kù))是獨(dú)立的系統(tǒng),更新的時(shí)候,無(wú)法做到原子性地操作兩個(gè)服務(wù),即有兩步操作來(lái)更新緩存服務(wù)和數(shù)據(jù)服務(wù)的數(shù)據(jù)。因此在并發(fā)讀寫以及第二步操作異常時(shí),會(huì)出現(xiàn)各種問(wèn)題。

此處說(shuō)的第二步操作,要么是操作緩存服務(wù),要么是操作數(shù)據(jù)服務(wù),根據(jù)具體的實(shí)現(xiàn)方式而定。

2. 緩存使用的誤區(qū)

  1. 服務(wù)與服務(wù)之間不要通過(guò)緩存?zhèn)鬟f數(shù)據(jù)

  2. 如果緩存掛掉,可能導(dǎo)致雪崩,此時(shí)要做高可用緩存,或者水平切分

  3. 調(diào)用方不宜再單獨(dú)使用緩存存儲(chǔ)服務(wù)底層的數(shù)據(jù),容易出現(xiàn)數(shù)據(jù)不一致,以及反向依賴

  4. 不同服務(wù),緩存實(shí)例要做垂直拆分

3. 常見(jiàn)更新模式

3.1. cache aside

同時(shí)更新緩存和數(shù)據(jù)庫(kù)
這是最常用的pattern了。其具體邏輯如下:

  • 數(shù)據(jù)查詢:應(yīng)用程序先從cache取數(shù)據(jù),沒(méi)有得到,則從數(shù)據(jù)庫(kù)中取數(shù)據(jù),成功后,放到緩存中。
  • 數(shù)據(jù)更新:先把數(shù)據(jù)存到數(shù)據(jù)庫(kù)中,成功后,再讓緩存失效。
    此處,更新的時(shí)候是先數(shù)據(jù)庫(kù),后緩存,也有另一種思路,先緩存,后數(shù)據(jù)庫(kù)。兩種思路詳見(jiàn)后文分析。

3.1.1. 先數(shù)據(jù)庫(kù)后緩存

這是最常用最常用的pattern。
分析異常情況如下:

  • \color{blue}{并發(fā)讀寫}
    • 一個(gè)是查詢請(qǐng)求,一個(gè)是更新請(qǐng)求的并發(fā)。更新請(qǐng)求在寫完數(shù)據(jù)庫(kù)之后,讓緩存失效之前,查詢請(qǐng)求到來(lái),此時(shí),緩存依然有效,所以,并發(fā)的查詢請(qǐng)求拿的是沒(méi)有更新的數(shù)據(jù),但是,隨后的更新請(qǐng)求會(huì)馬上讓緩存的失效了,在這之后的查詢請(qǐng)求再把數(shù)據(jù)從數(shù)據(jù)庫(kù)中取出來(lái),放入到緩存。\color{green}{不會(huì)出現(xiàn)不一致。}
    • 同樣是一個(gè)查詢請(qǐng)求,一個(gè)更新請(qǐng)求的并發(fā)。查詢請(qǐng)求 cache miss,讀了數(shù)據(jù)庫(kù),在將數(shù)據(jù)放到緩存之前,更新請(qǐng)求到來(lái),更新了數(shù)據(jù)庫(kù)內(nèi)容且讓緩存失效,此時(shí)查詢請(qǐng)求再將之前讀的老數(shù)據(jù)放入到緩存。這樣也會(huì)導(dǎo)致緩存和數(shù)據(jù)不一致。\color{red}{出現(xiàn)不一致,只是概率很小。} 為了避免這種情況,可以采取延遲失效,同時(shí)讀請(qǐng)求回填 cache 的時(shí)候,如果遇到 key 存在,則不更新,只能等待超時(shí)失效之后,讀請(qǐng)求回填的 cache 才可以更新;也可以考慮延遲雙刪,緩存失效后,等待1s 再失效一次緩存。
  • \color{red}{第二步操作異常}:如果是更新請(qǐng)求在寫完數(shù)據(jù)庫(kù),讓緩存失效之前,更新請(qǐng)求發(fā)起方異常(如宕機(jī)),此時(shí)數(shù)據(jù)庫(kù)已經(jīng)更新了,但是緩存一直沒(méi)有更新。導(dǎo)致查詢請(qǐng)求從緩存獲取的數(shù)據(jù)一直都是老數(shù)據(jù)。為了規(guī)避第二步操作異常,有兩種方法:1. 考慮在緩存上加上失效時(shí)間,但是這和周期性的更新緩存類似,還是會(huì)對(duì)系統(tǒng)性能還是有較大的影響;2. 使用可靠的消息隊(duì)列記錄(如 binlog)更改,然后消費(fèi)消息進(jìn)行緩存失效

這是標(biāo)準(zhǔn)的design pattern,包括Facebook的論文《Scaling Memcache at Facebook》也使用了這個(gè)策略。

3.1.2. 先緩存后數(shù)據(jù)庫(kù)

分析如下:

  • \color{red}{并發(fā)讀寫}:一個(gè)是查詢請(qǐng)求,一個(gè)是更新請(qǐng)求的并發(fā)。在更新請(qǐng)求讓緩存失效之后,寫數(shù)據(jù)庫(kù)之前,查詢操作到來(lái),此時(shí),會(huì)從數(shù)據(jù)庫(kù)讀到老數(shù)據(jù)進(jìn)而寫回到緩存,等更新請(qǐng)求寫完數(shù)據(jù)庫(kù),\color{red}{就會(huì)出現(xiàn)不一致。}

  • \color{red}{第二步操作異常}:如果是更新請(qǐng)求將緩存失效后,在寫數(shù)據(jù)庫(kù)前,更新操作發(fā)起方異常(如宕機(jī)),此時(shí)僅僅是 cache miss,\color{green}{不會(huì)導(dǎo)致不一致。}

針對(duì)第二種方案,我們可以采取以下兩種方案規(guī)避并發(fā)讀寫的問(wèn)題:

  1. 在使緩存失效的時(shí)候,不是立即失效,而是采取延遲失效(比如說(shuō)設(shè)置緩存失效時(shí)間為1s),保證在數(shù)據(jù)庫(kù)更新之前,讀請(qǐng)求依舊能夠命中cache,進(jìn)而不會(huì)出現(xiàn)讀請(qǐng)求更新 cache 的情況。這個(gè)方案應(yīng)該是最佳方案。
  2. 延遲雙刪。第一次緩存失效且數(shù)據(jù)庫(kù)操作完成之后,延遲再失效一次緩存。

3.1.3. 更新緩存還是失效緩存

為什么不是更新緩存,而是失效緩存?你可以看一下Quora上的這個(gè)問(wèn)答《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?》,主要是\color{red}{怕兩個(gè)并發(fā)的更新操作導(dǎo)致臟數(shù)據(jù).}

3.1.3. 結(jié)論

個(gè)人覺(jué)得,先緩存后數(shù)據(jù)庫(kù),配合延遲失效的方案最好。歡迎讀者討論。

3.2. Read/Write Through Pattern

先更新緩存,緩存負(fù)責(zé)同步更新數(shù)據(jù)庫(kù)。

在上面的 Cache Aside 更新模式中,應(yīng)用代碼需要維護(hù)兩個(gè)數(shù)據(jù)存儲(chǔ),一個(gè)是緩存(Cache),一個(gè)是數(shù)據(jù)庫(kù)(Repository)。而在Read/Write Through 更新模式中,應(yīng)用程序只需要維護(hù)緩存,數(shù)據(jù)庫(kù)的維護(hù)工作由緩存代理了。


write/read through流程圖

3.2.1. Read Through

Read Through 模式就是在查詢操作中更新緩存,也就是說(shuō),當(dāng)緩存失效的時(shí)候,Cache Aside 模式是由調(diào)用方負(fù)責(zé)把數(shù)據(jù)加載入緩存,而 Read Through 則用緩存服務(wù)自己來(lái)加載。

3.2.2. Write Through

Write Through 模式和 Read Through 相仿,不過(guò)是在更新數(shù)據(jù)時(shí)發(fā)生。當(dāng)有數(shù)據(jù)更新的時(shí)候,如果沒(méi)有命中緩存,直接更新數(shù)據(jù)庫(kù),然后返回。如果命中了緩存,則更新緩存,然后由緩存自己更新數(shù)據(jù)庫(kù)(這是一個(gè)同步操作)。

3.3. Write Behind Caching Pattern

先更新緩存,緩存定時(shí)異步更新數(shù)據(jù)庫(kù)。

Write Behind Caching 更新模式就是在更新數(shù)據(jù)的時(shí)候,只更新緩存,不更新數(shù)據(jù)庫(kù),而我們的緩存會(huì)異步地批量更新數(shù)據(jù)庫(kù)。這個(gè)設(shè)計(jì)的好處就是直接操作內(nèi)存速度快。因?yàn)楫惒剑琖rite Behind Caching 更新模式還可以合并對(duì)同一個(gè)數(shù)據(jù)的多次操作到數(shù)據(jù)庫(kù),所以性能的提高是相當(dāng)可觀的。

但其帶來(lái)的問(wèn)題是,數(shù)據(jù)不是強(qiáng)一致性的,而且可能會(huì)丟失。另外,Write Behind Caching 更新模式實(shí)現(xiàn)邏輯比較復(fù)雜,因?yàn)樗枰_認(rèn)有哪些數(shù)據(jù)是被更新了的,哪些數(shù)據(jù)需要刷到持久層上。只有在緩存需要失效的時(shí)候,才會(huì)把它真正持久起來(lái)。

write behind 流程圖

3.4. 總結(jié)

三種緩存模式的優(yōu)缺點(diǎn):

  • Cache Aside 更新模式實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單,但是需要維護(hù)兩個(gè)數(shù)據(jù)存儲(chǔ),一個(gè)是緩存(Cache),一個(gè)是數(shù)據(jù)庫(kù)(Repository)。
  • Read/Write Through 更新模式只需要維護(hù)一個(gè)數(shù)據(jù)存儲(chǔ)(緩存),但是實(shí)現(xiàn)起來(lái)要復(fù)雜一些。
  • Write Behind Caching 更新模式和Read/Write Through 更新模式類似,區(qū)別是Write Behind Caching 更新模式的數(shù)據(jù)持久化操作是異步的,但是Read/Write Through 更新模式的數(shù)據(jù)持久化操作是同步的。優(yōu)點(diǎn)是直接操作內(nèi)存速度快,多次操作可以合并持久化到數(shù)據(jù)庫(kù)。缺點(diǎn)是數(shù)據(jù)可能會(huì)丟失,例如系統(tǒng)斷電等。

緩存是通過(guò)犧牲強(qiáng)一致性來(lái)提高性能的。所以使用緩存提升性能,就是會(huì)有數(shù)據(jù)更新的延遲。這需要我們?cè)谠O(shè)計(jì)時(shí)結(jié)合業(yè)務(wù)仔細(xì)思考是否適合用緩存。然后緩存一定要設(shè)置過(guò)期時(shí)間,這個(gè)時(shí)間太短太長(zhǎng)都不好,太短的話請(qǐng)求可能會(huì)比較多的落到數(shù)據(jù)庫(kù)上,這也意味著失去了緩存的優(yōu)勢(shì)。太長(zhǎng)的話緩存中的臟數(shù)據(jù)會(huì)使系統(tǒng)長(zhǎng)時(shí)間處于一個(gè)延遲的狀態(tài),而且系統(tǒng)中長(zhǎng)時(shí)間沒(méi)有人訪問(wèn)的數(shù)據(jù)一直存在內(nèi)存中不過(guò)期,浪費(fèi)內(nèi)存。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容