若是第二次看到個人文章 ,歡迎點擊文末連接掃碼訂閱我我的的公衆號(跨界架構師)喲~每週五11:45 按時送達。固然了,也會時不時加個餐~
程序員本文長度爲4209字,建議閱讀12分鐘。redis
堅持原創,每一篇都是用心之做~數據庫
在前一篇《360°全方位解讀「緩存」》中,咱們聊了運用緩存的三種思路,以及在一個完整的系統中能夠設立緩存的幾個位置,而且分享了關於瀏覽器緩存、CDN緩存、網關(代理)緩存的一些使用經驗。瀏覽器
此次Z哥將深刻到實際場景中,來看一下「進程內緩存」、「進程外緩存」運用時的一些最佳實踐。因爲篇幅緣由,此次先聊三個問題。緩存
首當其衝的就是「先寫DB仍是緩存?」。我想,只要你開始運用緩存,這會是你第一個要好好思考的問題,不然在前方等待你的就是災難。。。bash
一個程序能夠沒有緩存,可是必定要有數據庫。這是你們的廣泛觀點,因此數據庫的重要性在你的潛意識裏老是被放在了第一位。微信
若是不細想的話你可能會以爲,數據庫操做失敗了,天然緩存也不用操做了;數據庫操做成功了,再操做緩存,沒毛病。網絡
可是數據庫操做成功,緩存操做的失敗的狀況該怎麼解?(主要在用到redis,memcached這種進程外緩存的時候,因爲網絡因素,失敗的可能性大增)多線程
辦法也是有的,在操做數據庫的時候帶一個事務,若是緩存操做失敗則事務回滾。大體的代碼意思以下:架構
begin trans
var isDbSuccess = write db;
if(isDbSuccess){
var isCacheSuccess = write cache;
if(isCacheSuccess){
return success;
}
else{
rollback db;
return fail;
}
}
else{
return fail;
}
catch(Exception ex){
rollback db;
}
end trans複製代碼
如此一來就萬無一失了嗎?並非。除了因爲事務的引入,增長了數據庫的壓力以外,在極端場景下可能會出現rollback db失敗的狀況。是否是很頭疼?
解決這個問題的方式就是write cache的時候作delete操做,而不是set操做。如此一來,用多一次cache miss的代價來換rollback db失敗的問題。
就像圖上所示,哪怕rollback失敗了,經過一次cache miss從新從db中載入舊值。
題外話:其實這種作法有一種專業的叫法——Cache Aside Pattern。爲了便於記憶,你能夠和分佈式系統的CAP定理同時記憶,叫「緩存的CAP模式」。
是否是看上去妥了?能夠開始瀟灑了?
若是你的數據庫沒有作高可用的話,的確能夠妥了。可是若是數據庫作了高可用,就會涉及到主從數據庫的數據同步,這就有新問題了。
題外話:因此你們不要過分追求技術的酷炫,可能會得不償失,自找麻煩。
什麼問題呢?就是若是在數據還未同步到「從庫」的時候,因爲cache miss去「從庫」取到了未同步前的舊值。
解決它的第一個方式很簡單,也很粗暴。就是定時去「從庫」讀數據,發現數據和緩存不同了就set到緩存裏去。
可是這個方式有點「治標不治本」。不斷的從數據庫定時讀取,對資源的消耗大不說,這個間隔頻率也很差定義一個比較合適的統一標準,過短吧,會致使重複讀取的次數加大,太長吧,又會致使緩存和數據庫不一致的時間變長。
因此這個方案僅適用於項目中只有二、3處須要作這種處理的場景,而且還不能是數據會頻繁修改的狀況。由於在數據修改頻次較高的場景,甚至可能還會出現這個定時機制所消耗的資源反而大於主程序的狀況。
通常狀況下,另外一種更普適性的方案是採用接下去聊的這種更底層的方式進行,就是「哪裏有問題處理哪裏」,當「從庫」完成同步的時候再額外作一次delete cache或者set cache的操做。
如此,雖然說也沒有100%解決短暫的數據不一致問題,可是已經將髒數據所存在的時長降到了最低(最終由主從同步的耗時決定),而且大大減小了無謂的資源消耗。
可能你會說,「不行,這麼一點時間也不能忍」怎麼辦?辦法是有,可是會增長「主庫」的壓力。就是在產生數據庫寫入動做後的一小段時間內強制讀「主庫」來加載緩存。
怎麼實現呢?先得依賴一個共享存儲,能夠藉助數據庫或者也能夠是咱們如今正在聊的分佈式緩存。
而後,你在事務提交以後往共享存儲中臨時存一個{ key = dbname + tablename + id,value = null,expire = 3s }這樣的數據,而且再作一次delete cache的操做。
begin trans
var isDbSuccess = write db;
if(isDbSuccess){
var isCacheSuccess = delete cache;
if(isCacheSuccess){
return success;
}
else{
rollback db;
return fail;
}
}
else{
return fail;
}
catch(Exception ex){
rollback db;
}
end trans
//在這裏作這個臨時存儲,{key,value,expire}。
delete cache;複製代碼
如此一來,當「讀數據」的時候發生cache miss,先判斷是否存在這個臨時數據,只要在3秒內就會強制走「主庫」取數據。
能夠看到,不一樣的方案各有利弊,須要根據具體的場景仔細權衡。
你工做中的大部分場景對數據準確性確定是低容忍的,因此通常不建議選擇「先緩存再DB」的方案,由於內存是易失性的。一旦遇到操做緩存成功,操做DB失敗的狀況,問題就來了。
在這個時候最新的數據只有緩存裏有,怎麼辦?單獨起個線程不斷的重試往數據庫寫?這個方案在必定程度上可行,但不適合用於對數據準確性有高要求的場景,由於緩存一旦掛了,數據就丟了!
題外話:哪怕選擇了這個方案,重試線程應確保只有1個,不然會存在「ABBA」的「併發寫」問題。
可能你會說用delete cache不就沒問題了?
能夠是能夠,可是要有個前提條件,訪問緩存的程序不會產生併發。由於只要你的程序是多線程運行的,一旦出現併發就有可能出現「讀」的線程因爲cache miss從數據庫取的時候,「寫」的線程還沒將數據寫到數據庫的狀況。
因此,哪怕用delete cache的方式,要麼帶lock(多客戶端狀況下還得上分佈式鎖),要麼必然出現數據不一致。
值得注意的是,若是數據庫一樣作了高可用,哪怕帶了lock,也還須要考慮和上面提到的「先DB再緩存」中同樣的因爲主從同步的時間差可能會產生的問題。
固然了,「先緩存再DB」也不是一文不值。當對寫入速度有極致要求,而對數據準確性沒那麼高要求的場景下就很是好使,其實就是前一篇(《360°全方位解讀「緩存」》)提到的「延遲寫」機制。
小結一下,相比緩存來講,數據庫的「高可用」通常會在系統發展的後期纔會引入,因此在沒有引入數據庫「高可用」的狀況下,Z哥建議你使用「先DB再緩存」的方式,而且緩存操做用delete而不是set,這樣基本就能夠高枕無憂了。
可是若是數據庫作了「高可用」,那麼團隊必然也造成必定規模了,這個時候就老老實實的作數據庫變動記錄(binlog)的訂閱吧。
到這裏可能有的小夥伴要問了,「若是上了分佈式緩存,還須要本地緩存嗎?」。
在解答這個問題以前咱們先來思考一個問題,一個分佈式系統最重要的價值是什麼?
是「無限擴展」,只要堆硬件就能應對業務增加。要達到這點的背後須要知足一個特性,就是程序要「無狀態」。那麼既想引入緩存來加速,又要達到「無狀態」,靠的就是分佈式緩存。
因此,能用分佈式緩存解決的問題就儘可能不要引入本地緩存。不然引入分佈式緩存的做用就小了不少。
可是在少數場景下,本地緩存仍是能夠發揮其價值的,可是咱們須要仔細識別出來。主要是三個場景:
不常常變動的數據。(好比一天甚至好幾天更新一次的那種)
須要支撐很是高的併發。(好比秒殺)
對數據準確性能容忍的場景。(好比瀏覽量,評論數等)
不過,我仍是建議你,除了第二種場景,不然仍是儘可能不要引入本地緩存。緣由咱們下面來講說。
其實這個緣由的根本問題就是在引入了本地緩存後,本地緩存(進程內緩存)、分佈式緩存(進程外緩存)、數據庫這三者之間的數據一致性該怎麼進行呢?
若是是個單點應用程序的話,很簡單,將本地緩存的操做放在最後就行了。
可能你會說本地緩存修改失敗怎麼辦?好比重複key啊什麼的異常。那你能夠反思一下爲這種數據爲何能夠成功的寫進數據庫。。。
可是,本地緩存帶來的一個巨大問題就是:雖然一個節點沒問題,可是多個本地緩存節點之間的數據如何同步?
解決這個問題的方式中有兩種和以前咱們聊過的Session問題(《作了「負載均衡」就能夠隨便加機器了嗎?》)是相似的。要麼是由接收修改的節點通知其它節點變動(經過rpc或者mq皆可),要麼藉助一致性hash讓同一個來源的請求固定落到一個節點上。後者可讓不一樣節點上的本地緩存數據都不重複,從源頭上避免了這個問題。
可是這兩個方案走的都是極端,前者變動成本過高,好比須要通知上千個節點的話,這個成本難以接受。然後者的話對資源的消耗過高,並且還容易出現壓力分攤不均勻的問題。因此,通常系統規模小的時候能夠考慮前者,而規模越大越會選擇後者。
還有一種相對中庸一些的,以下降數據的準確性來換成本的方案。就是設置緩存定時過時或者定時往下游的分佈式緩存拉取最新數據。這和前面「先DB再緩存」中提到的定時機制是同樣的邏輯,勝在簡單,缺點就是會存在更長時間的數據不一致。
小結一下,本地緩存的數據一致性解決方案,能完全解決的是藉助一致性hash的方案,可是成本比較高。因此,如非必要仍是慎重決定要不要作本地緩存。
好了,咱們一塊兒總結一下。
此次呢,Z哥先花了大量的篇幅和你討論「先寫DB仍是緩存」的問題,而且帶你層層深刻,經過一點一點的演進來闡述不一樣的解決方案。
而後與你討論了「本地緩存」的意義以及如何在「分佈式緩存」和「數據庫」的基礎上作好數據一致性,這其中主要是多個本地緩存節點之間的數據同步問題。
但願對你有所啓發。
此次的緩存實踐是一個很是好的例子,從中咱們能夠看到一件事情的精細化所帶來的複雜度須要更加的精細化去解決,可是又會帶來新的複雜度。因此做爲技術人的你,須要無時無刻考慮該怎麼權衡,而不是人云亦云。
相關文章:
若是你喜歡這篇文章,能夠點一下左側的「大拇指」哦~。
這樣能夠給我一點反饋。: )
謝謝你的舉手之勞。
▶關於做者:張帆(Zachary,我的微信號:Zachary-ZF)。堅持用心打磨每一篇高質量原創。本文首發於公衆號:「跨界架構師」(ID:Zachary_ZF)。<-- 點擊後閱讀熱門文章
按期發表原創內容:架構設計丨分佈式系統丨產品丨運營丨一些思考。
若是你是初級程序員,想提高但不知道如何下手。又或者作程序員多年,陷入了一些瓶頸想拓寬一下視野。歡迎關注個人公衆號「跨界架構師」,回覆「技術」,送你一份我長期收集和整理的思惟導圖。
若是你是運營,面對不斷變化的市場一籌莫展。又或者想了解主流的運營策略,以豐富本身的「倉庫」。歡迎關注個人公衆號「跨界架構師」,回覆「運營」,送你一份我長期收集和整理的思惟導圖。