背景
在高并發(fā)的分布式的系統(tǒng)中其障,緩存是必不可少的一部分银室。沒有緩存對系統(tǒng)的加速和阻擋大量的請求直接落到系統(tǒng)的底層,系統(tǒng)是很難撐住高并發(fā)的沖擊静秆,所以分布式系統(tǒng)中緩存的設(shè)計是很重要的一環(huán)
作用
使用緩存我們得到以下收益:整體是利的
- 加速讀寫粮揉。因為緩存通常是全內(nèi)存的巡李,比如Redis抚笔、Memcache。對內(nèi)存的直接讀寫會比傳統(tǒng)的存儲層如MySQL侨拦,性能好很多殊橙。舉個例子:同等配置單機Redis QPS可輕松上萬,MySQL則只有幾千狱从。加速讀寫之后膨蛮,響應(yīng)時間加快,相比之下系統(tǒng)的用戶體驗?zāi)艿玫礁玫奶嵘?/li>
- 降低后端的負載季研。緩存一些復(fù)雜計算或者耗時得出的結(jié)果可以降低后端系統(tǒng)對CPU敞葛、IO、線程這些資源的需求与涡,讓系統(tǒng)運行在一個相對資源健康的環(huán)境惹谐。
但隨之以來也有一些成本:
- 數(shù)據(jù)不一致性:緩存層與存儲層的數(shù)據(jù)存在著一定時間窗口一致,時間窗口與緩存的過期時間更新策略有關(guān)驼卖。
- 代碼維護成本:加入緩存后氨肌,需要同時處理緩存層和存儲層的邏輯,增加了開發(fā)者維護代碼的成本酌畜。
- 運維成本:引入緩存層怎囚,比如Redis。為保證高可用桥胞,需要做主從恳守,高并發(fā)需要做集群考婴。
產(chǎn)品
主要是redis,redis常見問題見redis文章分類
緩存讀寫策略
Cache Aside Pattern(旁路緩存模式)
Cache Aside Pattern 是我們平時使用比較多的一個緩存讀寫模式井誉,比較適合讀請求比較多的場景
寫:
- 先更新 DB
- 然后直接刪除 cache
讀 :
- 從 cache 中讀取數(shù)據(jù)蕉扮,讀取到就直接返回
- cache 中讀取不到的話,就從 DB 中讀取數(shù)據(jù)返回
- 再把數(shù)據(jù)放到 cache 中
Read/Write Through Pattern(讀寫穿透)
寫(Write Through):
- 先查 cache颗圣,cache 中不存在喳钟,直接更新 DB
- cache 中存在,則先更新 cache在岂,然后 cache 服務(wù)自己更新 DB(同步更新 cache 和 DB)
讀(Read Through):
- 從 cache 中讀取數(shù)據(jù)奔则,讀取到就直接返回
- 讀取不到的話,先從 DB 加載蔽午,寫入到 cache 后返回響應(yīng)
Write Behind Pattern(異步緩存寫入)
Write Behind Pattern 和 Read/Write Through Pattern 很相似易茬,兩者都是由 cache 服務(wù)來負責(zé) cache 和 DB 的讀寫。
但是及老,兩個又有很大的不同:Read/Write Through 是同步更新 cache 和 DB抽莱,而 Write Behind Caching 則是只更新緩存,不直接更新 DB骄恶,而是改為異步批量的方式來更新 DB食铐。
數(shù)據(jù)庫緩存一致性
不一致場景
我們業(yè)務(wù)中采用的先更新數(shù)據(jù)庫,再刪緩存這種情況不存在并發(fā)問題么僧鲁?
不是的虐呻。假設(shè)這會有兩個請求,一個請求A做查詢操作寞秃,一個請求B做更新操作斟叼,那么會有如下情形產(chǎn)生
(1)緩存剛好失效
(2)請求A查詢數(shù)據(jù)庫,得一個舊值
(3)請求B將新值寫入數(shù)據(jù)庫
(4)請求B刪除緩存
(5)請求A將查到的舊值寫入緩存
但是這樣的情況很少春寿,但數(shù)據(jù)庫讀寫分離情況下出現(xiàn)不一致的情況會更高:
(1)請求B更新數(shù)據(jù)庫
(2)請求B刪除緩存
(3)請求A讀數(shù)據(jù)走從庫朗涩,此時主從還未同步,A查到舊值
(4)請求A將查到的舊值寫入緩存
以上情況都是并發(fā)情況下無法避免的绑改,除非讀寫加鎖谢床,如果無法忍受則不適合采用緩存
方案
方案并不是要完全避免數(shù)據(jù)庫緩存一致性的情況,但可以很大程度上降低數(shù)據(jù)庫緩存一致性帶來的影響
延遲雙刪
刪除緩存的同時绢淀,另起一個線程在一段時間(可以是數(shù)據(jù)庫主從延遲的時間多一些)后再次刪除緩存萤悴,可以采用的雙刪方案非常多:
- JDK延遲線程池ScheduledThreadPoolExecutor
- MQ消息隊列
- Canal監(jiān)聽+MQ消息隊列
緩存問題
緩存穿透
一般的緩存系統(tǒng),都是按照key去緩存查詢皆的,如果不存在對應(yīng)的value覆履,就應(yīng)該去后端系統(tǒng)查找(比如DB)。如果key對應(yīng)的value是一定不存在的,并且對該key并發(fā)請求量很大硝全,就會對后端系統(tǒng)造成很大的壓力栖雾。這就叫做緩存穿透。
解決:
1伟众、對查詢結(jié)果為空的情況也進行緩存析藕,設(shè)置一個較短的過期時間
2、布隆過濾器:有誤判率凳厢,會將不存在的誤判為存在账胧,不建議使用
緩存擊穿
某些熱點緩存數(shù)據(jù)突然過期,同一時刻大量請求同時進入DB先紫,對數(shù)據(jù)庫造成極大壓力
解決:
1治泥、熱點數(shù)據(jù)不過期
2、后臺定時任務(wù)刷新
3遮精、分布式鎖只允許一個線程請求DB并刷新緩存
緩存雪崩
當(dāng)緩存服務(wù)器重啟或者大量緩存集中在某一個時間段失效居夹,這樣在失效的時候,也會給后端系統(tǒng)(比如DB)帶來很大壓力本冲。
解決:
1:在緩存失效后准脂,通過加鎖或者隊列來控制讀數(shù)據(jù)庫寫緩存的線程數(shù)量。比如對某個key只允許一個線程查詢數(shù)據(jù)和寫緩存檬洞,其他線程等待狸膏。
2:不同的key,設(shè)置不同的過期時間疮胖,讓緩存失效的時間點盡量均勻环戈。
布隆過濾器
如何判斷一個元素是不是在一個集合里
如果想判斷一個元素是不是在一個集合里闷板,一般想到的是將集合中所有元素保存起來澎灸,然后通過比較確定。鏈表遮晚、樹性昭、散列表(又叫哈希表,Hash table)等等數(shù)據(jù)結(jié)構(gòu)都是這種思路县遣。但是隨著集合中元素的增加糜颠,我們需要的存儲空間越來越大。同時檢索速度也越來越慢萧求。
原理
Bloom Filter 是一種空間效率很高的隨機數(shù)據(jù)結(jié)構(gòu)其兴,Bloom filter 可以看做是對 bit-map 的擴展, 它的原理是:
當(dāng)一個元素被加入集合時,通過 K 個 Hash 函數(shù)將這個元素映射成一個位陣列(Bit array)中的 K 個點夸政,把它們置為 1元旬。檢索時,我們只要看看這些點是不是都是 1 就(大約)知道集合中有沒有它了:
如果這些點有任何一個 0,則被檢索元素一定不在匀归;
如果都是 1坑资,則被檢索元素很可能在。
優(yōu)點
It tells us that the element either definitely is not in the set or may be in the set.
它的優(yōu)點是空間效率和查詢時間都遠遠超過一般的算法穆端,布隆過濾器存儲空間和插入 / 查詢時間都是常數(shù)O(k)袱贮。另外, 散列函數(shù)相互之間沒有關(guān)系,方便由硬件并行實現(xiàn)体啰。布隆過濾器不需要存儲元素本身攒巍,在某些對保密要求非常嚴(yán)格的場合有優(yōu)勢。
缺點
但是布隆過濾器的缺點和優(yōu)點一樣明顯荒勇。誤算率是其中之一窑业。隨著存入的元素數(shù)量增加,誤算率隨之增加枕屉。但是如果元素數(shù)量太少常柄,則使用散列表足矣。
(誤判補救方法是:再建立一個小的白名單搀擂,存儲那些可能被誤判的信息西潘。)
另外,一般情況下不能從布隆過濾器中刪除元素哨颂。
誤判率
增加位數(shù)組長度喷市、哈希函數(shù)數(shù)量可減少誤判率