13 | 緩存的使用姿勢(shì)(一):如何選擇緩存的讀寫策略饭庞?

今天戒悠,我們先講講緩存的讀寫策略。你可能覺得緩存的讀寫很簡(jiǎn)單舟山,只需要優(yōu)先讀緩存绸狐,緩存不命中就從數(shù)據(jù)庫查詢,查詢到了就回種緩存累盗。實(shí)際上寒矿,針對(duì)不同的業(yè)務(wù)場(chǎng)景,緩存的讀寫策略也是不同的若债。

而我們?cè)谶x擇策略時(shí)也需要考慮諸多的因素符相,比如說,緩存中是否有可能被寫入臟數(shù)據(jù)蠢琳,策略的讀寫性能如何啊终,是否存在緩存命中率下降的情況等等。接下來傲须,我就以標(biāo)準(zhǔn)的“緩存 + 數(shù)據(jù)庫”的場(chǎng)景為例蓝牲,帶你剖析經(jīng)典的緩存讀寫策略以及它們適用的場(chǎng)景躏碳。這樣一來搞旭,你就可以在日常的工作中根據(jù)不同的場(chǎng)景選擇不同的讀寫策略。

Cache Aside(旁路緩存)策略

我們來考慮一種最簡(jiǎn)單的業(yè)務(wù)場(chǎng)景菇绵,比方說在你的電商系統(tǒng)中有一個(gè)用戶表肄渗,表中只有 ID 和年齡兩個(gè)字段,緩存中我們以 ID 為 Key 存儲(chǔ)用戶的年齡信息咬最。那么當(dāng)我們要把 ID 為 1 的用戶的年齡從 19 變更為 20翎嫡,要如何做呢?

你可能會(huì)產(chǎn)生這樣的思路:先更新數(shù)據(jù)庫中 ID 為 1 的記錄永乌,再更新緩存中 Key 為 1 的數(shù)據(jù)惑申。

img

這個(gè)思路會(huì)造成緩存和數(shù)據(jù)庫中的數(shù)據(jù)不一致。

比如翅雏,A 請(qǐng)求將數(shù)據(jù)庫中 ID 為 1 的用戶年齡從 19 變更為 20圈驼,與此同時(shí),請(qǐng)求 B 也開始更新 ID 為 1 的用戶數(shù)據(jù)望几,它把數(shù)據(jù)庫中記錄的年齡變更為 21绩脆,然后變更緩存中的用戶年齡為 21。緊接著,A 請(qǐng)求開始更新緩存數(shù)據(jù)靴迫,它會(huì)把緩存中的年齡變更為 20惕味。此時(shí),數(shù)據(jù)庫中用戶年齡是 21玉锌,而緩存中的用戶年齡卻是 20名挥。

img

為什么產(chǎn)生這個(gè)問題呢?因?yàn)樽兏鼣?shù)據(jù)庫和變更緩存是兩個(gè)獨(dú)立的操作主守,而我們并沒有對(duì)操作做任何的并發(fā)控制禀倔。那么當(dāng)兩個(gè)線程并發(fā)更新它們的時(shí)候,就會(huì)因?yàn)閷懭腠樞虻牟煌斐蓴?shù)據(jù)的不一致参淫。

這個(gè)過程中也會(huì)有并發(fā)的問題蹋艺,比如說原有金額是 20,A 請(qǐng)求從緩存中讀到數(shù)據(jù)黄刚,并且把金額加 1,變更成 21民效,在未寫入緩存之前又有請(qǐng)求 B 也讀到緩存的數(shù)據(jù)后把金額也加 1憔维,也變更成 21,兩個(gè)請(qǐng)求同時(shí)把金額寫回緩存畏邢,這時(shí)緩存里面的金額是 21业扒,但是我們實(shí)際上預(yù)期是金額數(shù)加 2,這也是一個(gè)比較大的問題舒萎。

那我們要如何解決這個(gè)問題呢程储?其實(shí),我們可以在更新數(shù)據(jù)時(shí)不更新緩存臂寝,而是刪除緩存中的數(shù)據(jù)章鲤,在讀取數(shù)據(jù)時(shí),發(fā)現(xiàn)緩存中沒了數(shù)據(jù)之后咆贬,再從數(shù)據(jù)庫中讀取數(shù)據(jù)败徊,更新到緩存中。

img

這個(gè)策略就是我們使用緩存最常見的策略掏缎,Cache Aside 策略(也叫旁路緩存策略)皱蹦,這個(gè)策略數(shù)據(jù)以數(shù)據(jù)庫中的數(shù)據(jù)為準(zhǔn),緩存中的數(shù)據(jù)是按需加載的眷蜈。它可以分為讀策略和寫策略沪哺,其中

讀策略的步驟是:

  • 從緩存中讀取數(shù)據(jù);
  • 如果緩存命中酌儒,則直接返回?cái)?shù)據(jù)辜妓;
  • 如果緩存不命中,則從數(shù)據(jù)庫中查詢數(shù)據(jù);
  • 查詢到數(shù)據(jù)后嫌拣,將數(shù)據(jù)寫入到緩存中柔袁,并且返回給用戶。

寫策略的步驟是:

  • 更新數(shù)據(jù)庫中的記錄异逐;
  • 刪除緩存記錄捶索。

你也許會(huì)問了,在寫策略中灰瞻,能否先刪除緩存腥例,后更新數(shù)據(jù)庫呢?答案是不行的酝润,因?yàn)檫@樣也有可能出現(xiàn)緩存數(shù)據(jù)不一致的問題燎竖,我以用戶表的場(chǎng)景為例解釋一下。

假設(shè)某個(gè)用戶的年齡是 20要销,請(qǐng)求 A 要更新用戶年齡為 21构回,所以它會(huì)刪除緩存中的內(nèi)容。這時(shí)疏咐,另一個(gè)請(qǐng)求 B 要讀取這個(gè)用戶的年齡纤掸,它查詢緩存發(fā)現(xiàn)未命中后,會(huì)從數(shù)據(jù)庫中讀取到年齡為 20浑塞,并且寫入到緩存中借跪,然后請(qǐng)求 A 繼續(xù)更改數(shù)據(jù)庫,將用戶的年齡更新為 21酌壕,這就造成了緩存和數(shù)據(jù)庫的不一致掏愁。

img

那么像 Cache Aside 策略這樣先更新數(shù)據(jù)庫,后刪除緩存就沒有問題了嗎卵牍?其實(shí)在理論上還是有缺陷的果港。假如某個(gè)用戶數(shù)據(jù)在緩存中不存在,請(qǐng)求 A 讀取數(shù)據(jù)時(shí)從數(shù)據(jù)庫中查詢到年齡為 20辽慕,在未寫入緩存中時(shí)另一個(gè)請(qǐng)求 B 更新數(shù)據(jù)京腥。它更新數(shù)據(jù)庫中的年齡為 21,并且清空緩存溅蛉。這時(shí)請(qǐng)求 A 把從數(shù)據(jù)庫中讀到的年齡為 20 的數(shù)據(jù)寫入到緩存中公浪,造成緩存和數(shù)據(jù)庫數(shù)據(jù)不一致。

img

不過這種問題出現(xiàn)的幾率并不高船侧,原因是緩存的寫入通常遠(yuǎn)遠(yuǎn)快于數(shù)據(jù)庫的寫入欠气,所以在實(shí)際中很難出現(xiàn)請(qǐng)求 B 已經(jīng)更新了數(shù)據(jù)庫并且清空了緩存,請(qǐng)求 A 才更新完緩存的情況镜撩。而一旦請(qǐng)求 A 早于請(qǐng)求 B 清空緩存之前更新了緩存预柒,那么接下來的請(qǐng)求就會(huì)因?yàn)榫彺鏋榭斩鴱臄?shù)據(jù)庫中重新加載數(shù)據(jù)队塘,所以不會(huì)出現(xiàn)這種不一致的情況。

Cache Aside 策略是我們?nèi)粘i_發(fā)中最經(jīng)常使用的緩存策略宜鸯,不過我們?cè)谑褂脮r(shí)也要學(xué)會(huì)依情況而變憔古。比如說當(dāng)新注冊(cè)一個(gè)用戶,按照這個(gè)更新策略淋袖,你要寫數(shù)據(jù)庫鸿市,然后清理緩存(當(dāng)然緩存中沒有數(shù)據(jù)給你清理)〖赐耄可當(dāng)我注冊(cè)用戶后立即讀取用戶信息焰情,并且數(shù)據(jù)庫主從分離時(shí)剥懒,會(huì)出現(xiàn)因?yàn)橹鲝难舆t所以讀不到用戶信息的情況内舟。

而解決這個(gè)問題的辦法恰恰是在插入新數(shù)據(jù)到數(shù)據(jù)庫之后寫入緩存,這樣后續(xù)的讀請(qǐng)求就會(huì)從緩存中讀到數(shù)據(jù)了初橘。并且因?yàn)槭切伦?cè)的用戶验游,所以不會(huì)出現(xiàn)并發(fā)更新用戶信息的情況。

Cache Aside 存在的最大的問題是當(dāng)寫入比較頻繁時(shí)保檐,緩存中的數(shù)據(jù)會(huì)被頻繁地清理批狱,這樣會(huì)對(duì)緩存的命中率有一些影響。如果你的業(yè)務(wù)對(duì)緩存命中率有嚴(yán)格的要求展东,那么可以考慮兩種解決方案:

  1. 一種做法是在更新數(shù)據(jù)時(shí)也更新緩存,只是在更新緩存前先加一個(gè)分布式鎖炒俱,因?yàn)檫@樣在同一時(shí)間只允許一個(gè)線程更新緩存盐肃,就不會(huì)產(chǎn)生并發(fā)問題了。當(dāng)然這么做對(duì)于寫入的性能會(huì)有一些影響权悟;
  2. 另一種做法同樣也是在更新數(shù)據(jù)時(shí)更新緩存砸王,只是給緩存加一個(gè)較短的過期時(shí)間,這樣即使出現(xiàn)緩存不一致的情況峦阁,緩存的數(shù)據(jù)也會(huì)很快過期谦铃,對(duì)業(yè)務(wù)的影響也是可以接受。

當(dāng)然了榔昔,除了這個(gè)策略驹闰,在計(jì)算機(jī)領(lǐng)域還有其他幾種經(jīng)典的緩存策略,它們也有各自適用的使用場(chǎng)景撒会。

這個(gè)策略的核心原則是用戶只與緩存打交道嘹朗,由緩存和數(shù)據(jù)庫通信,寫入或者讀取數(shù)據(jù)诵肛。這就好比你在匯報(bào)工作的時(shí)候只對(duì)你的直接上級(jí)匯報(bào)屹培,再由你的直接上級(jí)匯報(bào)給他的上級(jí),你是不能越級(jí)匯報(bào)的。

Write Through 的策略是這樣的:先查詢要寫入的數(shù)據(jù)在緩存中是否已經(jīng)存在褪秀,如果已經(jīng)存在蓄诽,則更新緩存中的數(shù)據(jù),并且由緩存組件同步更新到數(shù)據(jù)庫中媒吗,如果緩存中數(shù)據(jù)不存在仑氛,我們把這種情況叫做“Write Miss(寫失效)”。

一般來說蝴猪,我們可以選擇兩種“Write Miss”方式:一個(gè)是“Write Allocate(按寫分配)”待诅,做法是寫入緩存相應(yīng)位置,再由緩存組件同步更新到數(shù)據(jù)庫中邦尊;另一個(gè)是“No-write allocate(不按寫分配)”腿椎,做法是不寫入緩存中,而是直接更新到數(shù)據(jù)庫中沛豌。

在 Write Through 策略中趋箩,我們一般選擇“No-write allocate”方式,原因是無論采用哪種“Write Miss”方式加派,我們都需要同步將數(shù)據(jù)更新到數(shù)據(jù)庫中叫确,而“No-write allocate”方式相比“Write Allocate”還減少了一次緩存的寫入,能夠提升寫入的性能芍锦。

Read Through 策略就簡(jiǎn)單一些竹勉,它的步驟是這樣的:先查詢緩存中數(shù)據(jù)是否存在,如果存在則直接返回娄琉,如果不存在次乓,則由緩存組件負(fù)責(zé)從數(shù)據(jù)庫中同步加載數(shù)據(jù)。

下面是 Read Through/Write Through 策略的示意圖:

img

Read Through/Write Through 策略的特點(diǎn)是由緩存節(jié)點(diǎn)而非用戶來和數(shù)據(jù)庫打交道孽水,在我們開發(fā)過程中相比 Cache Aside 策略要少見一些票腰,原因是我們經(jīng)常使用的分布式緩存組件女气,無論是 Memcached 還是 Redis 都不提供寫入數(shù)據(jù)庫杏慰,或者自動(dòng)加載數(shù)據(jù)庫中的數(shù)據(jù)的功能。而我們?cè)谑褂帽镜鼐彺娴臅r(shí)候可以考慮使用這種策略炼鞠,比如說在上一節(jié)中提到的本地緩存 Guava Cache 中的 Loading Cache 就有 Read Through 策略的影子缘滥。

我們看到 Write Through 策略中寫數(shù)據(jù)庫是同步的,這對(duì)于性能來說會(huì)有比較大的影響谒主,因?yàn)橄啾扔趯懢彺嫱暧颍綄憯?shù)據(jù)庫的延遲就要高很多了。那么我們可否異步地更新數(shù)據(jù)庫瘩将?這就是我們接下來要提到的“Write Back”策略吟税。

Write Back(寫回)策略

這個(gè)策略的核心思想是在寫入數(shù)據(jù)時(shí)只寫入緩存凹耙,并且把緩存塊兒標(biāo)記為“臟”的。而臟塊兒只有被再次使用時(shí)才會(huì)將其中的數(shù)據(jù)寫入到后端存儲(chǔ)中肠仪。

需要注意的是肖抱,在“Write Miss”的情況下,我們采用的是“Write Allocate”的方式异旧,也就是在寫入后端存儲(chǔ)的同時(shí)要寫入緩存意述,這樣我們?cè)谥蟮膶懻?qǐng)求中都只需要更新緩存即可,而無需更新后端存儲(chǔ)了吮蛹,我將 Write back 策略的示意圖放在了下面:

img

如果使用 Write Back 策略的話荤崇,讀的策略也有一些變化了。我們?cè)谧x取緩存時(shí)如果發(fā)現(xiàn)緩存命中則直接返回緩存數(shù)據(jù)潮针。如果緩存不命中則尋找一個(gè)可用的緩存塊兒术荤,如果這個(gè)緩存塊兒是“臟”的,就把緩存塊兒中之前的數(shù)據(jù)寫入到后端存儲(chǔ)中每篷,并且從后端存儲(chǔ)加載數(shù)據(jù)到緩存塊兒瓣戚,如果不是臟的,則由緩存組件將后端存儲(chǔ)中的數(shù)據(jù)加載到緩存中焦读,最后我們將緩存設(shè)置為不是臟的子库,返回?cái)?shù)據(jù)就好了。

img

發(fā)現(xiàn)了嗎矗晃?其實(shí)這種策略不能被應(yīng)用到我們常用的數(shù)據(jù)庫和緩存的場(chǎng)景中仑嗅,它是計(jì)算機(jī)體系結(jié)構(gòu)中的設(shè)計(jì),比如我們?cè)谙虼疟P中寫數(shù)據(jù)時(shí)采用的就是這種策略张症。無論是操作系統(tǒng)層面的 Page Cache无畔,還是日志的異步刷盤,亦或是消息隊(duì)列中消息的異步寫入磁盤吠冤,大多采用了這種策略。因?yàn)檫@個(gè)策略在性能上的優(yōu)勢(shì)毋庸置疑恭理,它避免了直接寫磁盤造成的隨機(jī)寫問題拯辙,畢竟寫內(nèi)存和寫磁盤的隨機(jī) I/O 的延遲相差了幾個(gè)數(shù)量級(jí)呢。

但因?yàn)榫彺嬉话闶褂脙?nèi)存颜价,而內(nèi)存是非持久化的涯保,所以一旦緩存機(jī)器掉電,就會(huì)造成原本緩存中的臟塊兒數(shù)據(jù)丟失周伦。所以你會(huì)發(fā)現(xiàn)系統(tǒng)在掉電之后夕春,之前寫入的文件會(huì)有部分丟失,就是因?yàn)?Page Cache 還沒有來得及刷盤造成的专挪。

當(dāng)然及志,你依然可以在一些場(chǎng)景下使用這個(gè)策略片排,在使用時(shí),我想給你的落地建議是:你在向低速設(shè)備寫入數(shù)據(jù)的時(shí)候速侈,可以在內(nèi)存里先暫存一段時(shí)間的數(shù)據(jù)率寡,甚至做一些統(tǒng)計(jì)匯總,然后定時(shí)地刷新到低速設(shè)備上倚搬。比如說冶共,你在統(tǒng)計(jì)你的接口響應(yīng)時(shí)間的時(shí)候,需要將每次請(qǐng)求的響應(yīng)時(shí)間打印到日志中每界,然后監(jiān)控系統(tǒng)收集日志后再做統(tǒng)計(jì)捅僵。但是如果每次請(qǐng)求都打印日志無疑會(huì)增加磁盤 I/O,那么不如把一段時(shí)間的響應(yīng)時(shí)間暫存起來眨层,經(jīng)過簡(jiǎn)單的統(tǒng)計(jì)平均耗時(shí)庙楚,每個(gè)耗時(shí)區(qū)間的請(qǐng)求數(shù)量等等,然后定時(shí)地谐岁,批量地打印到日志中醋奠。

劃重點(diǎn)

1.Cache Aside 是我們?cè)谑褂梅植际骄彺鏁r(shí)最常用的策略,你可以在實(shí)際工作中直接拿來使用伊佃。

2.Read/Write Through 和 Write Back 策略需要緩存組件的支持窜司,所以比較適合你在實(shí)現(xiàn)本地緩存組件的時(shí)候使用;

3.Write Back 策略是計(jì)算機(jī)體系結(jié)構(gòu)中的策略航揉,不過寫入策略中的只寫緩存塞祈,異步寫入后端存儲(chǔ)的策略倒是有很多的應(yīng)用場(chǎng)景。而且帅涂,你還需要了解议薪,我們今天提到的策略都是標(biāo)準(zhǔn)的

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市媳友,隨后出現(xiàn)的幾起案子斯议,更是在濱河造成了極大的恐慌,老刑警劉巖醇锚,帶你破解...
    沈念sama閱讀 211,348評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哼御,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡焊唬,警方通過查閱死者的電腦和手機(jī)恋昼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來赶促,“玉大人液肌,你說我怎么就攤上這事∨副酰” “怎么了嗦哆?”我有些...
    開封第一講書人閱讀 156,936評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵谤祖,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我吝秕,道長(zhǎng)泊脐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,427評(píng)論 1 283
  • 正文 為了忘掉前任烁峭,我火速辦了婚禮容客,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘约郁。我一直安慰自己缩挑,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,467評(píng)論 6 385
  • 文/花漫 我一把揭開白布鬓梅。 她就那樣靜靜地躺著供置,像睡著了一般。 火紅的嫁衣襯著肌膚如雪绽快。 梳的紋絲不亂的頭發(fā)上芥丧,一...
    開封第一講書人閱讀 49,785評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音坊罢,去河邊找鬼续担。 笑死,一個(gè)胖子當(dāng)著我的面吹牛活孩,可吹牛的內(nèi)容都是我干的物遇。 我是一名探鬼主播,決...
    沈念sama閱讀 38,931評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼憾儒,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼询兴!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起起趾,我...
    開封第一講書人閱讀 37,696評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤诗舰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后训裆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體眶根,經(jīng)...
    沈念sama閱讀 44,141評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,483評(píng)論 2 327
  • 正文 我和宋清朗相戀三年缭保,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蝙茶。...
    茶點(diǎn)故事閱讀 38,625評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡艺骂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出隆夯,到底是詐尸還是另有隱情钳恕,我是刑警寧澤别伏,帶...
    沈念sama閱讀 34,291評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站忧额,受9級(jí)特大地震影響厘肮,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜睦番,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,892評(píng)論 3 312
  • 文/蒙蒙 一类茂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧托嚣,春花似錦巩检、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至夫嗓,卻和暖如春迟螺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背舍咖。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來泰國打工矩父, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人谎仲。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓浙垫,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親郑诺。 傳聞我的和親對(duì)象是個(gè)殘疾皇子夹姥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,492評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容