在實際的開發(fā)當(dāng)中,我們經(jīng)常需要進行磁盤數(shù)據(jù)的讀取和搜索,因此經(jīng)常會有出現(xiàn)從數(shù)據(jù)庫讀取數(shù)據(jù)的場景出現(xiàn)。但是當(dāng)數(shù)據(jù)訪問量次數(shù)增大的時候,過多的磁盤讀取可能會最終成為整個系統(tǒng)的性能瓶頸,甚至是壓垮整個數(shù)據(jù)庫,導(dǎo)致系統(tǒng)卡死等嚴重問題。
常規(guī)的應(yīng)用系統(tǒng)中,我們通常會在需要的時候?qū)?shù)據(jù)庫進行查找,因此系統(tǒng)的大致結(jié)構(gòu)如下所示:

當(dāng)數(shù)據(jù)量較高的時候,需要減少對于數(shù)據(jù)庫里面的磁盤讀寫操作,因此通常都會選擇在業(yè)務(wù)系統(tǒng)和MySQL數(shù)據(jù)庫之間加入一層緩存從而減少對數(shù)據(jù)庫方面的訪問壓力。

但是很多時候,緩存在實際項目中的應(yīng)用并非這么簡單。下邊我們來通過幾個比較經(jīng)典的緩存應(yīng)用場景來列舉一些問題:
1.緩存和數(shù)據(jù)庫之間數(shù)據(jù)一致性問題
常用于緩存處理的機制我總結(jié)為了以下幾種:
Cache Aside
Read Through
Write Through
Write Behind Caching
首先來簡單說說Cache aside的這種方式:
Cache Aside模式
這種模式處理緩存通常都是先從數(shù)據(jù)庫緩存查詢,如果緩存沒有命中則從數(shù)據(jù)庫中進行查找。
這里面會發(fā)生的三種情況如下:
緩存命中:
當(dāng)查詢的時候發(fā)現(xiàn)緩存存在,那么直接從緩存中提取。
緩存失效:
當(dāng)緩存沒有數(shù)據(jù)的時候,則從database里面讀取源數(shù)據(jù),再加入到cache里面去。
緩存更新:
當(dāng)有新的寫操作去修改database里面的數(shù)據(jù)時,需要在寫操作完成之后,讓cache里面對應(yīng)的數(shù)據(jù)失效。
這種Cache aside模式通常是我們在實際應(yīng)用開發(fā)中最為常用到的模式。但是并非說這種模式的緩存處理就一定能做到完美。
關(guān)于這種模式下依然會存在缺陷。比如,一個是讀操作,但是沒有命中緩存,然后就到數(shù)據(jù)庫中取數(shù)據(jù),此時來了一個寫操作,寫完數(shù)據(jù)庫后,讓緩存失效,然后,之前的那個讀操作再把老的數(shù)據(jù)放進去,所以,會造成臟數(shù)據(jù)。
Facebook的大牛們也曾經(jīng)就緩存處理這個問題發(fā)表過相關(guān)的論文,鏈接如下:
https://www.usenix.org/system/files/conference/nsdi13/nsdi13-final170_update.pdf
分布式環(huán)境中要想完全的保證數(shù)據(jù)一致性是一件極為困難的事情,我們只能夠盡可能的減低這種數(shù)據(jù)不一致性問題產(chǎn)生的情況。
Read Through模式
Read Through模式是指應(yīng)用程序始終從緩存中請求數(shù)據(jù)。 如果緩存沒有數(shù)據(jù),則它負責(zé)使用底層提供程序插件從數(shù)據(jù)庫中檢索數(shù)據(jù)。 檢索數(shù)據(jù)后,緩存會自行更新并將數(shù)據(jù)返回給調(diào)用應(yīng)用程序。使用Read Through 有一個好處。
我們總是使用key從緩存中檢索數(shù)據(jù), 調(diào)用的應(yīng)用程序不知道數(shù)據(jù)庫, 由存儲方來負責(zé)自己的緩存處理,這使代碼更具可讀性, 代碼更清晰。但是這也有相應(yīng)的缺陷,開發(fā)人員需要給編寫相關(guān)的程序插件,增加了開發(fā)的難度性。
Write Through模式
Write Through模式和Read Through模式類似,當(dāng)數(shù)據(jù)發(fā)生更新的時候,先去Cache里面進行更新,如果命中了,則先更新緩存再由Cache方來更新database。如果沒有命中的話,就直接更新Cache里面的數(shù)據(jù)。
Write Behind Caching模式
Write Behind Caching 這種模式通常是先將數(shù)據(jù)寫入到緩存里面,然后再異步的寫入到database中進行數(shù)據(jù)同步,這樣的設(shè)計既可以直接的減少我們對于數(shù)據(jù)的database里面的直接訪問,降低壓力,同時對于database的多次修改可以進行合并操作,極大的提升了系統(tǒng)的承載能力。
但是這種模式處理緩存數(shù)據(jù)具有一定的風(fēng)險性,例如說當(dāng)cache機器出現(xiàn)宕機的時候,數(shù)據(jù)會有丟失的可能。
2.緩存穿透問題
在高并發(fā)的場景中,緩存穿透是一個經(jīng)常都會遇到的問題。
什么是緩存穿透?
大量的請求在緩存中沒有查詢到指定的數(shù)據(jù),因此需要從數(shù)據(jù)庫中進行查詢,造成緩存穿透。
會造成什么后果?
大量的請求短時間內(nèi)涌入到database中進行查詢會增加database的壓力,最終導(dǎo)致database無法承載客戶單請求的壓力,出現(xiàn)宕機卡死等現(xiàn)象。
常用的解決方案通常有以下幾類:
1.空值緩存
在某些特定的業(yè)務(wù)場景中,對于數(shù)據(jù)的查詢可能會是空的,沒有實際的存在,并且這類數(shù)據(jù)信息在短時間進行多次的反復(fù)查詢也不會有變化,那么整個過程中,多次的請求數(shù)據(jù)庫操作會顯得有些多余。
不妨可以將這些空值(沒有查詢結(jié)果的數(shù)據(jù))對應(yīng)的key存儲在緩存中,那么第二次查找的時候就不需要再次請求到database那么麻煩,只需要通過內(nèi)存查詢即可。這樣的做法能夠大大減少對于database的訪問壓力。
2.布隆過濾器
通常對于database里面的數(shù)據(jù)的key值可以預(yù)先存儲在布隆過濾器里面去,然后先在布隆過濾器里面進行過濾,如果發(fā)現(xiàn)布隆過濾器中沒有的話,就再去redis里面進行查詢,如果redis中也沒有數(shù)據(jù)的話,再去database查詢。這樣可以避免不存在的數(shù)據(jù)信息也去往存儲庫中進行查詢情況。

關(guān)于布隆過濾器的學(xué)習(xí)可以參考下我的這篇筆記:
https://blog.csdn.net/Danny_idea/article/details/88946673
3.緩存雪崩場景
什么是緩存雪崩?
當(dāng)緩存服務(wù)器重啟或者大量緩存集中在某一個時間段失效,這樣在失效的時候,也會給后端系統(tǒng)(比如DB)帶來很大壓力。
如何避免緩存雪崩問題?
1.使用加鎖隊列來應(yīng)付這種問題。當(dāng)有多個請求涌入的時候,當(dāng)緩存失效的時候加入一把分布式鎖,只允許搶鎖成功的請求去庫里面讀取數(shù)據(jù)然后將其存入緩存中,再釋放鎖,讓后續(xù)的讀請求從緩存中取數(shù)據(jù)。但是這種做法有一定的弊端,過多的讀請求線程堵塞,將機器內(nèi)存占滿,依然沒有能夠從根本上解決問題。
2.在并發(fā)場景發(fā)生前,先手動觸發(fā)請求,將緩存都存儲起來,以減少后期請求對database的第一次查詢的壓力。數(shù)據(jù)過期時間設(shè)置盡量分散開來,不要讓數(shù)據(jù)出現(xiàn)同一時間段出現(xiàn)緩存過期的情況。
3.從緩存可用性的角度來思考,避免緩存出現(xiàn)單點故障的問題,可以結(jié)合使用 主從+哨兵的模式來搭建緩存架構(gòu),但是這種模式搭建的緩存架構(gòu)有個弊端,就是無法進行緩存分片,存儲緩存的數(shù)據(jù)量有限制,因此可以升級為Redis Cluster架構(gòu)來進行優(yōu)化處理。(需要結(jié)合企業(yè)實際的經(jīng)濟實力,畢竟Redis Cluster的搭建需要更多的機器)
4.Ehcache本地緩存 + Hystrix限流&降級,避免MySQL被打死。
使用 Ehcache本地緩存的目的也是考慮在 Redis Cluster 完全不可用的時候,Ehcache本地緩存還能夠支撐一陣。
使用 Hystrix進行限流 & 降級 ,比如一秒來了5000個請求,我們可以設(shè)置假設(shè)只能有一秒 2000個請求能通過這個組件,那么其他剩余的 3000 請求就會走限流邏輯。
然后去調(diào)用我們自己開發(fā)的降級組件(降級),比如設(shè)置的一些默認值呀之類的。以此來保護最后的 MySQL 不會被大量的請求給打死。