redis/數(shù)據(jù)庫 雙寫一致性

轉(zhuǎn)

首先,緩存由于其高并發(fā)和高性能的特性改鲫,已經(jīng)在項(xiàng)目中被廣泛使用诈皿。在讀取緩存方面,大家沒啥疑問像棘,都是按照下圖的流程來進(jìn)行業(yè)務(wù)操作纫塌。

但是在更新緩存方面,對(duì)于更新完數(shù)據(jù)庫讲弄,是更新緩存呢,還是刪除緩存依痊。又或者是先刪除緩存避除,再更新數(shù)據(jù)庫怎披,其實(shí)大家存在很大的爭議。目前沒有一篇全面的博客瓶摆,對(duì)這幾種方案進(jìn)行解析凉逛。于是博主戰(zhàn)戰(zhàn)兢兢,頂著被大家噴的風(fēng)險(xiǎn)群井,寫了這篇文章状飞。

文章結(jié)構(gòu)

本文由以下三個(gè)部分組成

1、講解緩存更新策略
2书斜、對(duì)每種策略進(jìn)行缺點(diǎn)分析
3诬辈、針對(duì)缺點(diǎn)給出改進(jìn)方案

正文

先做一個(gè)說明,從理論上來說荐吉,給緩存設(shè)置過期時(shí)間焙糟,是保證最終一致性的解決方案。這種方案下样屠,我們可以對(duì)存入緩存的數(shù)據(jù)設(shè)置過期時(shí)間穿撮,所有的寫操作以數(shù)據(jù)庫為準(zhǔn),對(duì)緩存操作只是盡最大努力即可痪欲。也就是說如果數(shù)據(jù)庫寫成功悦穿,緩存更新失敗,那么只要到達(dá)過期時(shí)間业踢,則后面的讀請(qǐng)求自然會(huì)從數(shù)據(jù)庫中讀取新值然后回填緩存栗柒。因此,接下來討論的思路不依賴于給緩存設(shè)置過期時(shí)間這個(gè)方案陨亡。

在這里傍衡,我們討論三種更新策略:

  • 先更新數(shù)據(jù)庫,再更新緩存

  • 先刪除緩存负蠕,再更新數(shù)據(jù)庫

  • 先更新數(shù)據(jù)庫蛙埂,再刪除緩存

應(yīng)該沒人問我,為什么沒有先更新緩存遮糖,再更新數(shù)據(jù)庫這種策略绣的。

(1)先更新數(shù)據(jù)庫,再更新緩存

這套方案欲账,大家是普遍反對(duì)的屡江。為什么呢?有如下兩點(diǎn)原因赛不。

  • 原因一(線程安全角度)

同時(shí)有請(qǐng)求A和請(qǐng)求B進(jìn)行更新操作惩嘉,那么會(huì)出現(xiàn)

(1)線程A更新了數(shù)據(jù)庫
(2)線程B更新了數(shù)據(jù)庫
(3)線程B更新了緩存
(4)線程A更新了緩存

這就出現(xiàn)請(qǐng)求A更新緩存應(yīng)該比請(qǐng)求B更新緩存早才對(duì),但是因?yàn)榫W(wǎng)絡(luò)等原因踢故,B卻比A更早更新了緩存文黎。這就導(dǎo)致了臟數(shù)據(jù)惹苗,因此不考慮。

  • 原因二(業(yè)務(wù)場景角度)

有如下兩點(diǎn):

(1)如果你是一個(gè)寫數(shù)據(jù)庫場景比較多耸峭,而讀數(shù)據(jù)場景比較少的業(yè)務(wù)需求桩蓉,采用這種方案就會(huì)導(dǎo)致,數(shù)據(jù)壓根還沒讀到劳闹,緩存就被頻繁的更新院究,浪費(fèi)性能。

(2)如果你寫入數(shù)據(jù)庫的值本涕,并不是直接寫入緩存的业汰,而是要經(jīng)過一系列復(fù)雜的計(jì)算再寫入緩存。那么偏友,每次寫入數(shù)據(jù)庫后蔬胯,都再次計(jì)算寫入緩存的值,無疑是浪費(fèi)性能的位他。顯然氛濒,刪除緩存更為適合。

接下來討論的就是爭議最大的鹅髓,先刪緩存舞竿,再更新數(shù)據(jù)庫。還是先更新數(shù)據(jù)庫窿冯,再刪緩存的問題骗奖。

(2)先刪緩存,再更新數(shù)據(jù)庫

該方案會(huì)導(dǎo)致不一致的原因是醒串。同時(shí)有一個(gè)請(qǐng)求A進(jìn)行更新操作执桌,另一個(gè)請(qǐng)求B進(jìn)行查詢操作。那么會(huì)出現(xiàn)如下情形:

(1)請(qǐng)求A進(jìn)行寫操作芜赌,刪除緩存
(2)請(qǐng)求B查詢發(fā)現(xiàn)緩存不存在
(3)請(qǐng)求B去數(shù)據(jù)庫查詢得到舊值
(4)請(qǐng)求B將舊值寫入緩存
(5)請(qǐng)求A將新值寫入數(shù)據(jù)庫

上述情況就會(huì)導(dǎo)致不一致的情形出現(xiàn)仰挣。而且,如果不采用給緩存設(shè)置過期時(shí)間策略缠沈,該數(shù)據(jù)永遠(yuǎn)都是臟數(shù)據(jù)膘壶。

那么,如何解決呢洲愤?采用延時(shí)雙刪策略

偽代碼如下

public void write(String key,Object data){

        redis.delKey(key);

        db.updateData(data);

        Thread.sleep(1000);

        redis.delKey(key);

    }

轉(zhuǎn)化為中文描述就是

(1)先淘汰緩存
(2)再寫數(shù)據(jù)庫(這兩步和原來一樣)
(3)休眠1秒颓芭,再次淘汰緩存

這么做,可以將1秒內(nèi)所造成的緩存臟數(shù)據(jù)柬赐,再次刪除亡问。

那么,這個(gè)1秒怎么確定的肛宋,具體該休眠多久呢玛界?

針對(duì)上面的情形万矾,讀者應(yīng)該自行評(píng)估自己的項(xiàng)目的讀數(shù)據(jù)業(yè)務(wù)邏輯的耗時(shí)。然后寫數(shù)據(jù)的休眠時(shí)間則在讀數(shù)據(jù)業(yè)務(wù)邏輯的耗時(shí)基礎(chǔ)上慎框,加幾百ms即可。這么做的目的后添,就是確保讀請(qǐng)求結(jié)束笨枯,寫請(qǐng)求可以刪除讀請(qǐng)求造成的緩存臟數(shù)據(jù)。

如果你用了mysql的讀寫分離架構(gòu)怎么辦遇西?

ok馅精,在這種情況下,造成數(shù)據(jù)不一致的原因如下粱檀,還是兩個(gè)請(qǐng)求洲敢,一個(gè)請(qǐng)求A進(jìn)行更新操作,另一個(gè)請(qǐng)求B進(jìn)行查詢操作茄蚯。

(1)請(qǐng)求A進(jìn)行寫操作压彭,刪除緩存
(2)請(qǐng)求A將數(shù)據(jù)寫入數(shù)據(jù)庫了,
(3)請(qǐng)求B查詢緩存發(fā)現(xiàn)渗常,緩存沒有值
(4)請(qǐng)求B去從庫查詢壮不,這時(shí),還沒有完成主從同步皱碘,因此查詢到的是舊值
(5)請(qǐng)求B將舊值寫入緩存
(6)數(shù)據(jù)庫完成主從同步询一,從庫變?yōu)樾轮?/p>

上述情形,就是數(shù)據(jù)不一致的原因癌椿。還是使用雙刪延時(shí)策略健蕊。只是,睡眠時(shí)間修改為在主從同步的延時(shí)時(shí)間基礎(chǔ)上踢俄,加幾百ms缩功。

采用這種同步淘汰策略,吞吐量降低怎么辦褪贵?

ok掂之,那就將第二次刪除作為異步的。自己起一個(gè)線程脆丁,異步刪除世舰。這樣,寫的請(qǐng)求就不用沉睡一段時(shí)間后了槽卫,再返回跟压。這么做,加大吞吐量歼培。

第二次刪除,如果刪除失敗怎么辦震蒋?

這是個(gè)非常好的問題茸塞,因?yàn)榈诙蝿h除失敗,就會(huì)出現(xiàn)如下情形查剖。還是有兩個(gè)請(qǐng)求钾虐,一個(gè)請(qǐng)求A進(jìn)行更新操作,另一個(gè)請(qǐng)求B進(jìn)行查詢操作笋庄,為了方便效扫,假設(shè)是單庫:

(1)請(qǐng)求A進(jìn)行寫操作,刪除緩存
(2)請(qǐng)求B查詢發(fā)現(xiàn)緩存不存在
(3)請(qǐng)求B去數(shù)據(jù)庫查詢得到舊值
(4)請(qǐng)求B將舊值寫入緩存
(5)請(qǐng)求A將新值寫入數(shù)據(jù)庫
(6)請(qǐng)求A試圖去刪除請(qǐng)求B寫入對(duì)緩存值直砂,結(jié)果失敗了菌仁。

ok,這也就是說。如果第二次刪除緩存失敗静暂,會(huì)再次出現(xiàn)緩存和數(shù)據(jù)庫不一致的問題济丘。

如何解決呢?

具體解決方案洽蛀,且看博主對(duì)第(3)種更新策略的解析摹迷。

(3)先更新數(shù)據(jù)庫,再刪緩存

首先辱士,先說一下泪掀。老外提出了一個(gè)緩存更新套路,名為《Cache-Aside pattern》颂碘。其中就指出

失效:應(yīng)用程序先從cache取數(shù)據(jù)异赫,沒有得到,則從數(shù)據(jù)庫中取數(shù)據(jù)头岔,成功后塔拳,放到緩存中。

命中:應(yīng)用程序從cache中取數(shù)據(jù)峡竣,取到后返回靠抑。

更新:先把數(shù)據(jù)存到數(shù)據(jù)庫中,成功后适掰,再讓緩存失效颂碧。

另外,知名社交網(wǎng)站facebook也在論文《Scaling Memcache at Facebook》中提出类浪,他們用的也是先更新數(shù)據(jù)庫载城,再刪緩存的策略。

這種情況不存在并發(fā)問題么费就?

不是的诉瓦。假設(shè)這會(huì)有兩個(gè)請(qǐng)求,一個(gè)請(qǐng)求A做查詢操作,一個(gè)請(qǐng)求B做更新操作睬澡,那么會(huì)有如下情形產(chǎn)生

(1)緩存剛好失效
(2)請(qǐng)求A查詢數(shù)據(jù)庫固额,得一個(gè)舊值
(3)請(qǐng)求B將新值寫入數(shù)據(jù)庫
(4)請(qǐng)求B刪除緩存
(5)請(qǐng)求A將查到的舊值寫入緩存

ok,如果發(fā)生上述情況煞聪,確實(shí)是會(huì)發(fā)生臟數(shù)據(jù)斗躏。

然而,發(fā)生這種情況的概率又有多少呢米绕?

發(fā)生上述情況有一個(gè)先天性條件瑟捣,就是步驟(3)的寫數(shù)據(jù)庫操作比步驟(2)的讀數(shù)據(jù)庫操作耗時(shí)更短,才有可能使得步驟(4)先于步驟(5)栅干。可是捐祠,大家想想碱鳞,數(shù)據(jù)庫的讀操作的速度遠(yuǎn)快于寫操作的(不然做讀寫分離干嘛,做讀寫分離的意義就是因?yàn)樽x操作比較快踱蛀,耗資源少)窿给,因此步驟(3)耗時(shí)比步驟(2)更短,這一情形很難出現(xiàn)率拒。

假設(shè)崩泡,有人非要抬杠,有強(qiáng)迫癥猬膨,一定要解決怎么辦角撞?

如何解決上述并發(fā)問題?

首先勃痴,給緩存設(shè)有效時(shí)間是一種方案谒所。其次,采用策略(2)里給出的異步延時(shí)刪除策略沛申,保證讀請(qǐng)求完成以后劣领,再進(jìn)行刪除操作。

還有其他造成不一致的原因么铁材?

有的尖淘,這也是緩存更新策略(2)和緩存更新策略(3)都存在的一個(gè)問題,如果刪緩存失敗了怎么辦著觉,那不是會(huì)有不一致的情況出現(xiàn)么村生。比如一個(gè)寫數(shù)據(jù)請(qǐng)求,然后寫入數(shù)據(jù)庫了固惯,刪緩存失敗了梆造,這會(huì)就出現(xiàn)不一致的情況了。這也是緩存更新策略(2)里留下的最后一個(gè)疑問。

如何解決镇辉?

提供一個(gè)保障的重試機(jī)制即可屡穗,這里給出兩套方案。

方案一:

如下圖所示

流程如下所示

(1)更新數(shù)據(jù)庫數(shù)據(jù)忽肛;
(2)緩存因?yàn)榉N種問題刪除失敗
(3)將需要?jiǎng)h除的key發(fā)送至消息隊(duì)列
(4)自己消費(fèi)消息村砂,獲得需要?jiǎng)h除的key
(5)繼續(xù)重試刪除操作,直到成功

然而屹逛,該方案有一個(gè)缺點(diǎn)础废,對(duì)業(yè)務(wù)線代碼造成大量的侵入。于是有了方案二罕模,在方案二中评腺,啟動(dòng)一個(gè)訂閱程序去訂閱數(shù)據(jù)庫的binlog,獲得需要操作的數(shù)據(jù)淑掌。在應(yīng)用程序中蒿讥,另起一段程序,獲得這個(gè)訂閱程序傳來的信息抛腕,進(jìn)行刪除緩存操作芋绸。

方案二:

流程如下圖所示:

(1)更新數(shù)據(jù)庫數(shù)據(jù)
(2)數(shù)據(jù)庫會(huì)將操作信息寫入binlog日志當(dāng)中
(3)訂閱程序提取出所需要的數(shù)據(jù)以及key
(4)另起一段非業(yè)務(wù)代碼,獲得該信息
(5)嘗試刪除緩存操作担敌,發(fā)現(xiàn)刪除失敗
(6)將這些信息發(fā)送至消息隊(duì)列
(7)重新從消息隊(duì)列中獲得該數(shù)據(jù)摔敛,重試操作。

備注說明:上述的訂閱binlog程序在mysql中有現(xiàn)成的中間件叫canal全封,可以完成訂閱binlog日志的功能马昙。至于oracle中,博主目前不知道有沒有現(xiàn)成中間件可以使用售貌。另外给猾,重試機(jī)制,博主是采用的是消息隊(duì)列的方式颂跨。如果對(duì)一致性要求不是很高敢伸,直接在程序中另起一個(gè)線程,每隔一段時(shí)間去重試即可恒削,這些大家可以靈活自由發(fā)揮池颈,只是提供一個(gè)思路。

總結(jié)

本文其實(shí)是對(duì)目前互聯(lián)網(wǎng)中已有的一致性方案钓丰,進(jìn)行了一個(gè)總結(jié)躯砰。對(duì)于先刪緩存,再更新數(shù)據(jù)庫的更新策略携丁,還有方案提出維護(hù)一個(gè)內(nèi)存隊(duì)列的方式琢歇,博主看了一下兰怠,覺得實(shí)現(xiàn)異常復(fù)雜,沒有必要李茫,因此沒有必要在文中給出揭保。最后,希望大家有所收獲魄宏。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末秸侣,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子宠互,更是在濱河造成了極大的恐慌味榛,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,607評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件予跌,死亡現(xiàn)場離奇詭異搏色,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)券册,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門继榆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人汁掠,你說我怎么就攤上這事〖遥” “怎么了考阱?”我有些...
    開封第一講書人閱讀 164,960評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鞠苟。 經(jīng)常有香客問我乞榨,道長,這世上最難降的妖魔是什么当娱? 我笑而不...
    開封第一講書人閱讀 58,750評(píng)論 1 294
  • 正文 為了忘掉前任吃既,我火速辦了婚禮,結(jié)果婚禮上跨细,老公的妹妹穿的比我還像新娘鹦倚。我一直安慰自己,他們只是感情好冀惭,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,764評(píng)論 6 392
  • 文/花漫 我一把揭開白布震叙。 她就那樣靜靜地躺著,像睡著了一般散休。 火紅的嫁衣襯著肌膚如雪媒楼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,604評(píng)論 1 305
  • 那天戚丸,我揣著相機(jī)與錄音划址,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛夺颤,可吹牛的內(nèi)容都是我干的痢缎。 我是一名探鬼主播,決...
    沈念sama閱讀 40,347評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼拂共,長吁一口氣:“原來是場噩夢啊……” “哼牺弄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起宜狐,我...
    開封第一講書人閱讀 39,253評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤势告,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后抚恒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咱台,經(jīng)...
    沈念sama閱讀 45,702評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,893評(píng)論 3 336
  • 正文 我和宋清朗相戀三年俭驮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蛇尚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片升薯。...
    茶點(diǎn)故事閱讀 40,015評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出咆课,到底是詐尸還是另有隱情,我是刑警寧澤枉氮,帶...
    沈念sama閱讀 35,734評(píng)論 5 346
  • 正文 年R本政府宣布碰纬,位于F島的核電站,受9級(jí)特大地震影響崭倘,放射性物質(zhì)發(fā)生泄漏翼岁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,352評(píng)論 3 330
  • 文/蒙蒙 一司光、第九天 我趴在偏房一處隱蔽的房頂上張望琅坡。 院中可真熱鬧,春花似錦残家、人聲如沸榆俺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谴仙。三九已至,卻和暖如春碾盐,著一層夾襖步出監(jiān)牢的瞬間晃跺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評(píng)論 1 270
  • 我被黑心中介騙來泰國打工毫玖, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留掀虎,地道東北人凌盯。 一個(gè)月前我還...
    沈念sama閱讀 48,216評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像烹玉,于是被迫代替她去往敵國和親驰怎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,969評(píng)論 2 355

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