隨著互聯(lián)網(wǎng)的越來(lái)越普及,用戶越來(lái)越多,系統(tǒng)性能瓶頸成了越來(lái)越熱門(mén)的話題。要解決性能問(wèn)題的技術(shù)手段有很多,比如:緩存、CDN加速、頁(yè)面靜態(tài)化、集群、分布式、異步等。
緩存通常被作為首先技術(shù)方案,簡(jiǎn)單而且提升效果明顯,它能夠?qū)⑺俣忍嵘?00倍。那么問(wèn)題來(lái)了,緩存為啥會(huì)怎么快呢?
因?yàn)閭鹘y(tǒng)的數(shù)據(jù)庫(kù)操作是基于磁盤(pán)的,而緩存是基于內(nèi)存的,內(nèi)存操作和磁盤(pán)操作的速度根本不是一個(gè)數(shù)量級(jí)的。目前市面上主流的緩存有:redis 和 memcache,這兩個(gè)都是基于內(nèi)存的緩存技術(shù),二者的區(qū)別我在這里暫時(shí)不講。使用緩存的偽代碼一般如下:
String order = redisClient.get(key);
if(order != null) {
return order;
}
order = db.get(key);
redisClient.put(key,order);
redisClient.expire(key,3000);
return order;
根據(jù)key獲取數(shù)據(jù),先從緩存中查一下有沒(méi)有,如果有則直接返回。如果沒(méi)有,再?gòu)臄?shù)據(jù)庫(kù)中查到數(shù)據(jù),然后將數(shù)據(jù)放入緩存中,并且給當(dāng)前key設(shè)置一個(gè)失效時(shí)間,下次再用同樣的key來(lái)請(qǐng)求數(shù)據(jù)時(shí),就能夠直接從緩存中查詢到并返回,減少請(qǐng)求數(shù)據(jù)庫(kù)的頻次,提升性能,因?yàn)閿?shù)據(jù)庫(kù)連接是稀有資源。
那么問(wèn)題又來(lái)了,為啥要設(shè)置失效時(shí)間,不設(shè)置不行嗎?
著名的2/8原則告訴我們,經(jīng)常訪問(wèn)的數(shù)據(jù)集中在20%,而另外的80%屬于不常用數(shù)據(jù)。我們都知道內(nèi)存相當(dāng)于磁盤(pán)來(lái)說(shuō)價(jià)格是比較昂貴的,不信你買(mǎi)個(gè)500G的硬盤(pán) 和 一個(gè) 500G的內(nèi)存試試。既然這么貴,我們應(yīng)該節(jié)約使用,所以才會(huì)有設(shè)置失效時(shí)間這種策略,一旦檢測(cè)到某個(gè)key超過(guò)了失效時(shí)間,就會(huì)將該key從緩存中刪除,可以節(jié)約內(nèi)存。
還有個(gè)問(wèn)題:如果在某個(gè)key失效的時(shí)候,有大量的請(qǐng)求一起過(guò)來(lái)會(huì)怎么樣?
這就是我今天要給大家講的:擊穿。
大量的請(qǐng)求訪問(wèn)同一個(gè)key,剛好那個(gè)key失效了,那么同一時(shí)間所有的請(qǐng)求,都會(huì)穿過(guò)緩存,直接請(qǐng)求數(shù)據(jù)庫(kù),此時(shí)的數(shù)據(jù)庫(kù)有可能因?yàn)闊o(wú)法扛著這么大的并發(fā),直接掛了。
再問(wèn)一下:如果大量的請(qǐng)求訪問(wèn)多個(gè)key,剛好key同時(shí)失效了會(huì)怎么樣?
這就是我今天要給大家講的:雪崩。
雪崩比上面的擊穿更嚴(yán)重,擊穿只是一個(gè)key失效了,大量請(qǐng)求直接訪問(wèn)數(shù)據(jù)庫(kù)都有可能把數(shù)據(jù)庫(kù)搞掛,更何況大量的key同時(shí)失效的場(chǎng)景,數(shù)據(jù)庫(kù)面臨的壓力更大,更有可能掛掉。
接下來(lái)的問(wèn)題:如果大量的用戶請(qǐng)求緩存中不存在的key又會(huì)怎么樣?
這就是我今天要給大家講的:穿透。
有大量的請(qǐng)求訪問(wèn)時(shí),只有少部分的key在緩存中存在,而有大量的key不存在,這樣請(qǐng)求也會(huì)直接訪問(wèn)到數(shù)據(jù)庫(kù),也會(huì)導(dǎo)致數(shù)據(jù)庫(kù)扛不住壓力而掛掉。這種情況往往是黑客偽造請(qǐng)求,發(fā)起的惡意攻擊。
那么,這些問(wèn)題有沒(méi)有解決辦法呢?
首先,擊穿的解決辦法-加鎖。
偽代碼如下:
String order = redisClient.get(key);
if(order != null) {
return order;
}
lock() {
String order = redisClient.get(key);
if(order != null) {
return order;
}
order = db.get(key);
redisClient.put(key,order);
redisClient.expire(key,3000);
}
return order;
如果根據(jù)key從緩存中查詢不到數(shù)據(jù),需要從數(shù)據(jù)庫(kù)中查詢數(shù)據(jù)的時(shí)候,加一把鎖,保證同一時(shí)間只有一個(gè)線程可以查詢數(shù)據(jù)庫(kù),然后把查詢出來(lái)的結(jié)果放回到緩存中。這樣其他的線程再用相同的key查詢時(shí),就可以直接從緩存中查到數(shù)據(jù)。這樣就能夠極大的減少數(shù)據(jù)庫(kù)的訪問(wèn)頻次。
其次,雪崩的解決辦法- 加鎖 + key設(shè)置不同的失效時(shí)間。
加鎖的偽代碼跟上面是一樣的我就不寫(xiě)了。
雪崩還有一個(gè)必要條件就是在同一時(shí)間,有大量的key同時(shí)失效。我們只要保證不會(huì)出現(xiàn)同一時(shí)間有大量的key同時(shí)失效就可以了,每個(gè)key設(shè)置不同的失效時(shí)間就能解決問(wèn)題。
最后,穿透的解決辦法- 業(yè)務(wù)規(guī)則過(guò)濾 + 布隆過(guò)濾器
業(yè)務(wù)規(guī)則過(guò)濾 可以校驗(yàn) key的長(zhǎng)度或者比如前綴SD開(kāi)頭的等,過(guò)濾一批非法數(shù)據(jù)。
接下來(lái)看看布隆過(guò)濾器:
布隆過(guò)濾器中會(huì)初始化數(shù)據(jù)庫(kù)中key的標(biāo)識(shí)。如果有大量請(qǐng)求訪問(wèn)不存在的key時(shí),先通過(guò)布隆過(guò)濾器檢查一下key在數(shù)據(jù)庫(kù)中是否存在,如果存在才允許訪問(wèn)數(shù)據(jù)庫(kù)。如果不存在,則直接返回,這樣就可以過(guò)濾掉大量的非法請(qǐng)求。






