Redis緩存一致性設(shè)計(jì)筆記

Spring 注解使用:控制 Redis 緩存更新
使用 SpringBoot 可以很容易地對(duì) Redis 進(jìn)行操作。Java 的 Redis 的客戶端常用的有三個(gè):jedis边酒、redisson泞辐、lettuce贸宏。其中,Spring 默認(rèn)使用的是 lettuce。

很多人喜歡使用 Spring 抽象的緩存包 spring-cache钳垮,它可以使用注解,非常方便额港。它的注解采用 AOP 的方式饺窿,對(duì) Cache 層進(jìn)行了抽象,可以在各種堆內(nèi)緩存框架和分布式框架之間進(jìn)行切換移斩。

<dependency> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-cache</artifactId> 
</dependency>

使用 spring-cache 有三個(gè)步驟:

在啟動(dòng)類上加入 @EnableCaching 注解肚医;

使用 CacheManager 初始化要使用的緩存框架,使用 @CacheConfig 注解注入要使用的資源向瓷;

使用 @Cacheable 等注解對(duì)資源進(jìn)行緩存肠套。
而針對(duì)緩存操作的注解有三個(gè):

@Cacheable 表示如果緩存系統(tǒng)里沒有這個(gè)數(shù)值,就將方法的返回值緩存起來猖任;

@CachePut 表示每次執(zhí)行該方法你稚,都把返回值緩存起來;

@CacheEvict 表示執(zhí)行方法的時(shí)候超升,清除某些緩存值入宦。
非常簡(jiǎn)單,對(duì)緩存的操作也無非是 CRUD室琢。

一致性問題是如何產(chǎn)生的?

我們先看一下具體的 API 操作落追,緩存操作和數(shù)據(jù)庫的 CRUD 結(jié)合起來盈滴,我們可以抽象成下面幾個(gè)方法:

getFromDB(key)

getFromRedis(key)

putToDB(key,value)

putToRedis(key,value)

deleteFromDB(key)

deleteFromRedis(key)

把 Redis 作緩存用,就說明 Redis 是不適合作為落地存儲(chǔ)的轿钠。

我們一般把最終的數(shù)據(jù)存放在數(shù)據(jù)庫中巢钓。一般情況下,Redis 的操作速度比數(shù)據(jù)庫的操作速度快得多疗垛。畢竟是 10wQPS 和上千 QPS 的對(duì)比症汹。關(guān)于它們的速度,我們暫時(shí)可以畫一張圖贷腕,表明它們之間的速度差異背镇。


image.png
上面這些 API 很簡(jiǎn)單,但把它們的順序調(diào)整一下泽裳,一致性就會(huì)出現(xiàn)問題瞒斩。一致性,簡(jiǎn)單說就是“數(shù)據(jù)庫里的數(shù)據(jù)”與“Redis 中的數(shù)據(jù)”不一樣了涮总。

對(duì)于讀取過程胸囱,一般是沒有什么異議的。

首先瀑梗,讀緩存烹笔;

如果緩存里沒有值裳扯,那就讀取數(shù)據(jù)庫的值;

同時(shí)把這個(gè)值寫進(jìn)緩存中谤职。

我們下面主要看一下寫模式饰豺。

雙更新模式:操作不合理,導(dǎo)致數(shù)據(jù)一致性問題

public void putValue(key,value){
    putToRedis(key,value);
    putToDB(key,value);//操作失敗了
}

比如我要更新一個(gè)值柬帕,首先刷了緩存哟忍,然后把數(shù)據(jù)庫也更新了。但過程中陷寝,更新數(shù)據(jù)庫可能會(huì)失敗锅很,發(fā)生了回滾。所以凤跑,最后“緩存里的數(shù)據(jù)”和“數(shù)據(jù)庫的數(shù)據(jù)”就不一樣了爆安,也就是出現(xiàn)了數(shù)據(jù)一致性問題。

如果更新數(shù)據(jù)庫仔引,再更新緩存

public void putValue(key,value){
    putToDB(key,value);
    putToRedis(key,value);
}

考慮到下面的場(chǎng)景:操作 A 更新 a 的值為 1扔仓,操作 B 更新 a 的值為 2。由于數(shù)據(jù)庫和 Redis 的操作咖耘,并不是原子的翘簇,它們的執(zhí)行時(shí)長(zhǎng)也不是可控制的。當(dāng)兩個(gè)請(qǐng)求的時(shí)序發(fā)生了錯(cuò)亂儿倒,就會(huì)發(fā)生緩存不一致的情況版保。

image.png

放到實(shí)操中,就如上圖所示:A 操作在更新數(shù)據(jù)庫成功后夫否,再更新 Redis彻犁;但在更新 Redis 之前,另外一個(gè)更新操作 B 執(zhí)行完畢凰慈。那么操作 A 的這個(gè) Redis 更新動(dòng)作汞幢,就和數(shù)據(jù)庫里面的值不一樣了。

其實(shí)雙更新模式的問題微谓,主要不是體現(xiàn)在并發(fā)的一致性上森篷,而是業(yè)務(wù)操作的合理性上。

我們大多數(shù)業(yè)務(wù)代碼并沒有經(jīng)過良好的設(shè)計(jì)堰酿。一個(gè)緩存的值疾宏,可能是多條數(shù)據(jù)庫記錄拼湊或計(jì)算得出來的。比如一個(gè)余額操作触创,可能是“錢包里的值”加上“基金里的值”計(jì)算得出來的坎藐。

要是采用“更新”的方式,那這個(gè)計(jì)算代碼就分散在項(xiàng)目的多個(gè)地方,這就不合理了岩馍。

那么怎么辦呢碉咆?其實(shí),我們把“緩存更新”改成“刪除”就好了蛀恩。

“后刪緩存”能解決多數(shù)不一致

因?yàn)槊看巫x取時(shí)疫铜,如果判斷 Redis 里沒有值,就會(huì)重新讀取數(shù)據(jù)庫双谆,這個(gè)邏輯是沒問題的壳咕。唯一的問題是:我們是先刪除緩存?還是后刪除緩存顽馋?

1.如果先刪緩存

public void putValue(key,value){
    deleteFromRedis(key);
    putToDB(key,value);
}

就和上面的圖一樣谓厘。操作 B 刪除了某個(gè) key 的值,這時(shí)候有另外一個(gè)請(qǐng)求 A 到來寸谜,那么它就會(huì)擊穿到數(shù)據(jù)庫竟稳,讀取到舊的值。無論操作 B 更新數(shù)據(jù)庫的操作持續(xù)多長(zhǎng)時(shí)間熊痴,都會(huì)產(chǎn)生不一致的情況他爸。

2.如果后刪緩存

而把刪除的動(dòng)作放在后面,就能夠保證每次讀到的值都是新鮮的果善,從數(shù)據(jù)庫里面拿到最新的诊笤。

public void putValue(key,value){
    putToDB(key,value);
    deleteFromRedis(key);
}

這就是Cache-Aside Pattern,也是我們平常使用最多的模式巾陕。我們看一下它的具體方式盏混。

先看一下數(shù)據(jù)的讀取過程,規(guī)則是“先讀 cache惜论,再讀 db”,詳細(xì)步驟如下:

每次讀取數(shù)據(jù)止喷,都從 cache 里讀馆类;

如果讀到了,則直接返回弹谁,稱作 cache hit乾巧;

如果讀不到 cache 的數(shù)據(jù),則從 db 里面撈一份预愤,稱作 cache miss沟于;

將讀取到的數(shù)據(jù)塞入到緩存中,下次讀取時(shí)植康,就可以直接命中旷太。

再來看一下寫請(qǐng)求,規(guī)則是“先更新 db,再刪除緩存”供璧,詳細(xì)步驟如下:

1.將變更寫入到數(shù)據(jù)庫中存崖;

2.刪除緩存里對(duì)應(yīng)的數(shù)據(jù)。
為什么說最常用呢睡毒?因?yàn)?Spring cache 就是默認(rèn)實(shí)現(xiàn)了這個(gè)模式来惧。

Spring 的源碼。緩存的移除演顾,是在 Cache-Aside Pattern 中實(shí)現(xiàn)的


image.png

image.png

并發(fā)量更大時(shí)供搀,“后刪緩存”依舊不一致

所以在高并發(fā)情況下,Cache Aside Pattern 會(huì)不夠用钠至。下面就描述一個(gè)“先更新再刪除”這種場(chǎng)景下葛虐,依然會(huì)產(chǎn)生不一致的情況。場(chǎng)景很好理解棕洋、很極端挡闰,但在高并發(fā)多實(shí)例的情況下很常見。


image.png

如上圖所示掰盘,有一系列的高并發(fā)操作摄悯,一直執(zhí)行著更新、刪除的動(dòng)作愧捕。某個(gè)時(shí)刻奢驯,它更新數(shù)據(jù)庫的值為 1,然后刪除了緩存次绘。

正在這時(shí)瘪阁,有兩個(gè)請(qǐng)求發(fā)生了:

一個(gè)是讀操作,讀到的當(dāng)然是數(shù)據(jù)庫的舊值 1邮偎,我們記作操作 A管跺;

同時(shí),另外一個(gè)請(qǐng)求發(fā)起了更新操作禾进,把數(shù)據(jù)庫記錄更新為 2豁跑,我們記作操作 B。

一般情況下泻云,讀取操作都是比寫入操作快的艇拍,但我們要考慮兩種極端情況:

一種是這個(gè)讀取操作 A,發(fā)生在更新操作 B 的尾部宠纯;

一種是操作 A 的這個(gè) Redis 的操作時(shí)長(zhǎng)卸夕,耗費(fèi)了非常多的時(shí)間。比如婆瓜,這個(gè)節(jié)點(diǎn)正好發(fā)生了 STW快集。
那么很容易地,讀操作 A 的結(jié)束時(shí)間就超過了操作 B 刪除的動(dòng)作。就像上圖虛線部分畫的一樣碍讨,這個(gè)時(shí)候治力,數(shù)據(jù)也是不一致的。

實(shí)際上勃黍,你也無法控制它們的執(zhí)行順序宵统。只要發(fā)生這種情況,大概率數(shù)據(jù)庫和 Redis 的值會(huì)不一致覆获。

如何解決

1.延時(shí)雙刪

而假如我有一種機(jī)制马澈,能夠確保刪除動(dòng)作一定被執(zhí)行,那就可以解決問題弄息,起碼能縮小數(shù)據(jù)不一致的時(shí)間窗口痊班。常用的方法就是延時(shí)雙刪,依然是先更新再刪除摹量,唯一不同的是:我們把這個(gè)刪除動(dòng)作涤伐,在不久之后再執(zhí)行一次,比如 5 秒之后缨称。

public void putValue(key,value){
    putToDB(key,value);
    deleteFromRedis(key);

    ...deleteFromRedis(key,after5sec);
}

而刪除動(dòng)作也有多種選擇:

如果放在 DelayQueue 中凝果,會(huì)有隨著 JVM 進(jìn)程的死亡,丟失更新的風(fēng)險(xiǎn)睦尽;

如果放在 MQ 中器净,會(huì)增加編碼的復(fù)雜性。
所以到了這個(gè)時(shí)候当凡,并沒有一個(gè)能夠行走天下的解決方案山害。我們得綜合評(píng)價(jià)很多因素去做設(shè)計(jì),比如團(tuán)隊(duì)的水平沿量、工期浪慌、不一致的忍受程度等。

2.閃電緩存

還有一種不太常用的朴则,那就是采用閃電緩存眷射。就是把緩存的失效時(shí)間設(shè)置非常短,比如 3~4 秒佛掖。一旦失效,就會(huì)再次去數(shù)據(jù)庫讀取最新數(shù)據(jù)到緩存涌庭。但這種方式芥被,在非常高的并發(fā)下,同一時(shí)間對(duì)某個(gè) key 的請(qǐng)求擊穿到 DB坐榆,會(huì)鎖死數(shù)據(jù)庫拴魄,所以很少用。

對(duì)于一般并發(fā)場(chǎng)景,上面的各種修修補(bǔ)補(bǔ)匹中,已經(jīng)把不一致問題降低到很小的概率了夏漱。但是它仍然是有問題的,因?yàn)樗肓艘粋€(gè)高可用問題:緩存擊穿顶捷。

緩存擊穿

兩種不同的解決方式:

1.讀操作互斥

我們依然采用 Cache-Aside Pattern挂绰,只不過在讀的時(shí)候進(jìn)行一下處理。來看一下偽代碼服赎,從 Redis 讀取不到值的時(shí)候葵蒂,我們要上鎖去從數(shù)據(jù)庫中讀這個(gè)值。我們這里默認(rèn)這個(gè)值是有的重虑,否則就得處理緩存穿透的問題践付。

get(key){

    res = getFromRedis(key);

    //讀取緩存為null

    if(null == res){

        lock.lock(...);

        //再次讀取緩存為null

        res = getFromRedis(key);

        if(res == null){

            res = getFromDB(key);

            if(null != res){

                //讀取設(shè)值

                putToRedis(key,res);

            }

        }

        lock.unlock();

    }

    return res;

}

getFromDB(key){

    ...

}

使用分布式鎖和非分布式鎖的主要區(qū)別,還是在于數(shù)據(jù)一致性窗口上:

-對(duì)于多線程鎖來說缺厉,可能某些節(jié)點(diǎn)執(zhí)行得非常慢永高,更新了舊的值到 Redis;

-對(duì)于分布式鎖來說提针,肯定又是一個(gè)效率上的話題命爬。

2.集中更新

集中更新。這個(gè)很美好关贵,但大多數(shù)業(yè)務(wù)很復(fù)雜遇骑,這對(duì)業(yè)務(wù)架構(gòu)的前期設(shè)計(jì)要求非常高。比如通過 Binlog 方式揖曾,典型的如 Canal落萎。我們不會(huì)在代碼里做任何 Redis 更新的操作,而是會(huì)設(shè)計(jì)一個(gè)服務(wù)炭剪,訂閱最新的 binlog 更新信息练链,然后解析它們,主動(dòng)去更新緩存奴拦。這個(gè)一般在大并發(fā)大廠才會(huì)采用媒鼓。

還有一種就是弱化數(shù)據(jù)庫。所有的數(shù)據(jù)首先在 Redis 落地错妖,也就是把 Redis 作為數(shù)據(jù)庫使用绿鸣,把數(shù)據(jù)庫作為備份庫使用。有定時(shí)任務(wù)暂氯,定期把 Redis 中的數(shù)據(jù)潮模,保存到數(shù)據(jù)庫或其他地方。

一般痴施,重要業(yè)務(wù)還要配備一個(gè)對(duì)賬系統(tǒng)擎厢,定時(shí)去掃描究流,以便快速發(fā)現(xiàn)不一致的情況

小結(jié)

針對(duì) Redis 的緩存一致性問題,我們聊了很多动遭》姨剑可以看到,無論你怎么做厘惦,一致性問題總是存在偷仿,只是幾率慢慢變小了。

隨著對(duì)不一致問題的忍受程度越來越低绵估、并發(fā)量越來越高炎疆,我們所采用的方案也越來越極端。一般情況下国裳,到了延時(shí)雙刪這一步形入,就證明你的并發(fā)量已經(jīng)夠大了;再往下走缝左,無不是對(duì)高可用亿遂、成本、一致性的權(quán)衡渺杉,進(jìn)入到了特事特辦的場(chǎng)景蛇数,甚至要考慮基礎(chǔ)設(shè)施,關(guān)于這些每個(gè)公司的策略都是不一樣的是越。

除了 Cache-Aside Pattern耳舅,一致性常見的還有 Read-Through、Write-Through倚评、Write-Behind 等模式浦徊,它們都有自己的應(yīng)用場(chǎng)景

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市天梧,隨后出現(xiàn)的幾起案子盔性,更是在濱河造成了極大的恐慌,老刑警劉巖呢岗,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冕香,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡后豫,警方通過查閱死者的電腦和手機(jī)悉尾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來挫酿,“玉大人焕襟,你說我怎么就攤上這事》贡” “怎么了鸵赖?”我有些...
    開封第一講書人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)拄衰。 經(jīng)常有香客問我它褪,道長(zhǎng),這世上最難降的妖魔是什么翘悉? 我笑而不...
    開封第一講書人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任茫打,我火速辦了婚禮,結(jié)果婚禮上妖混,老公的妹妹穿的比我還像新娘老赤。我一直安慰自己,他們只是感情好制市,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開白布抬旺。 她就那樣靜靜地躺著,像睡著了一般祥楣。 火紅的嫁衣襯著肌膚如雪开财。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,578評(píng)論 1 305
  • 那天误褪,我揣著相機(jī)與錄音责鳍,去河邊找鬼。 笑死兽间,一個(gè)胖子當(dāng)著我的面吹牛历葛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播嘀略,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼恤溶,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了屎鳍?” 一聲冷哼從身側(cè)響起宏娄,我...
    開封第一講書人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎逮壁,沒想到半個(gè)月后孵坚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡窥淆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年卖宠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片忧饭。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡扛伍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出词裤,到底是詐尸還是另有隱情刺洒,我是刑警寧澤鳖宾,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站逆航,受9級(jí)特大地震影響鼎文,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜因俐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一拇惋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧抹剩,春花似錦撑帖、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至境蔼,卻和暖如春灶平,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背箍土。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來泰國打工逢享, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吴藻。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓瞒爬,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親沟堡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子侧但,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355

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