Redis缓存雪崩、缓存穿透、热点Key
我們通常使用 緩存 + 過(guò)期時(shí)間的策略來(lái)幫助我們加速接口的訪問(wèn)速度,減少了后端負(fù)載,同時(shí)保證功能的更新。
1、緩存穿透
緩存系統(tǒng),按照KEY去查詢VALUE,當(dāng)KEY對(duì)應(yīng)的VALUE一定不存在的時(shí)候并對(duì)KEY并發(fā)請(qǐng)求量很大的時(shí)候,就會(huì)對(duì)后端造成很大的壓力。
(查詢一個(gè)必然不存在的數(shù)據(jù)。比如文章表,查詢一個(gè)不存在的id,每次都會(huì)訪問(wèn)DB,如果有人惡意破壞,很可能直接對(duì)DB造成影響。)
由于緩存不命中,每次都要查詢持久層。從而失去緩存的意義。
1.1 使用互斥鎖排隊(duì)
業(yè)界比價(jià)普遍的一種做法,即根據(jù)key獲取value值為空時(shí),鎖上,從數(shù)據(jù)庫(kù)中l(wèi)oad數(shù)據(jù)后再釋放鎖。若其它線程獲取鎖失敗,則等待一段時(shí)間后重試。這里要注意,分布式環(huán)境中要使用分布式鎖,單機(jī)的話用普通的鎖(synchronized、Lock)就夠了。
public String getWithLock(String key, Jedis jedis, String lockKey,String uniqueId, long expireTime) {// 通過(guò)key獲取valueString value = redisService.get(key);if (StringUtil.isEmpty(value)) {// 分布式鎖,詳細(xì)可以參考https://blog.csdn.net/fanrenxiang/article/details/79803037// 封裝的tryDistributedLock包括setnx和expire兩個(gè)功能,在低版本的redis中不支持try {boolean locked = redisService.tryDistributedLock(jedis,lockKey, uniqueId, expireTime);if (locked) {value = userService.getById(key);redisService.set(key, value);redisService.del(lockKey);return value;} else {// 其它線程進(jìn)來(lái)了沒(méi)獲取到鎖便等待50ms后重試Thread.sleep(50);getWithLock(key, jedis, lockKey, uniqueId, expireTime);}} catch (Exception e) {log.error("getWithLock exception=" + e);return value;} finally {redisService.releaseDistributedLock(jedis, lockKey, uniqueId);}}return value;}這樣做思路比較清晰,也從一定程度上減輕數(shù)據(jù)庫(kù)壓力,但是鎖機(jī)制使得邏輯的復(fù)雜度增加,吞吐量也降低了,有點(diǎn)治標(biāo)不治本。
1.2 布隆過(guò)濾器
bloomfilter就類似于一個(gè)hash set,用于快速判某個(gè)元素是否存在于集合中,其典型的應(yīng)用場(chǎng)景就是快速判斷一個(gè)key是否存在于某容器,不存在就直接返回。布隆過(guò)濾器的關(guān)鍵就在于hash算法和容器大小,下面先來(lái)簡(jiǎn)單的實(shí)現(xiàn)下看看效果,我這里用guava實(shí)現(xiàn)的布隆過(guò)濾器:
<dependencies> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>23.0</version> </dependency> </dependencies> public class BloomFilterTest {private static final int capacity = 1000000;private static final int key = 999998;private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity);static {for (int i = 0; i < capacity; i++) {bloomFilter.put(i);}}public static void main(String[] args) {/* 返回計(jì)算機(jī)最精確的時(shí)間,單位微妙 */long start = System.nanoTime();if (bloomFilter.mightContain(key)) {System.out.println("成功過(guò)濾到" + key);}long end = System.nanoTime();System.out.println("布隆過(guò)濾器消耗時(shí)間:" + (end - start));int sum = 0;for (int i = capacity + 20000; i < capacity + 30000; i++) {if (bloomFilter.mightContain(i)) {sum = sum + 1;}}System.out.println("錯(cuò)判率為:" + sum);}}成功過(guò)濾到999998 布隆過(guò)濾器消耗時(shí)間:215518 錯(cuò)判率為:318 復(fù)可以看到,100w個(gè)數(shù)據(jù)中只消耗了約0.2毫秒就匹配到了key,速度足夠快。然后模擬了1w個(gè)不存在于布隆過(guò)濾器中的key,匹配錯(cuò)誤率為318/10000,也就是說(shuō),出錯(cuò)率大概為3%,跟蹤下BloomFilter的源碼發(fā)現(xiàn)默認(rèn)的容錯(cuò)率就是0.03
2、緩存雪崩問(wèn)題
緩存在同一時(shí)間內(nèi)大量鍵過(guò)期(失效),接著來(lái)的一大波請(qǐng)求瞬間都落在了數(shù)據(jù)庫(kù)中導(dǎo)致連接異常。
解決方案:
3、熱點(diǎn)key
(1) 這個(gè)key是一個(gè)熱點(diǎn)key(例如一個(gè)重要的新聞,一個(gè)熱門的八卦新聞等等),所以這種key訪問(wèn)量可能非常大。
(2) 緩存的構(gòu)建是需要一定時(shí)間的。(可能是一個(gè)復(fù)雜計(jì)算,例如復(fù)雜的sql、多次IO、多個(gè)依賴(各種接口)等等)
于是就會(huì)出現(xiàn)一個(gè)致命問(wèn)題:在緩存失效的瞬間,有大量線程來(lái)構(gòu)建緩存(見(jiàn)下圖),造成后端負(fù)載加大,甚至可能會(huì)讓系統(tǒng)崩潰 。
解決方法:使用互斥鎖(mutex key):這種解決方案思路比較簡(jiǎn)單,就是只讓一個(gè)線程構(gòu)建緩存,其他線程等待構(gòu)建緩存的線程執(zhí)行完,重新從緩存獲取數(shù)據(jù)就可以了
"提前"使用互斥鎖(mutex key):在value內(nèi)部設(shè)置1個(gè)超時(shí)值(timeout1), timeout1比實(shí)際的memcache timeout(timeout2)小。當(dāng)從cache讀取到timeout1發(fā)現(xiàn)它已經(jīng)過(guò)期時(shí)候,馬上延長(zhǎng)timeout1并重新設(shè)置到cache。然后再?gòu)臄?shù)據(jù)庫(kù)加載數(shù)據(jù)并設(shè)置到cache中。
“永遠(yuǎn)不過(guò)期”:
這里的“永遠(yuǎn)不過(guò)期”包含兩層意思:
(1) 從redis上看,確實(shí)沒(méi)有設(shè)置過(guò)期時(shí)間,這就保證了,不會(huì)出現(xiàn)熱點(diǎn)key過(guò)期問(wèn)題,也就是“物理”不過(guò)期。(2) 從功能上看,如果不過(guò)期,那不就成靜態(tài)的了嗎?所以我們把過(guò)期時(shí)間存在key對(duì)應(yīng)的value里,如果發(fā)現(xiàn)要過(guò)期了,通過(guò)一個(gè)后臺(tái)的異步線程進(jìn)行緩存的構(gòu)建,也就是“邏輯”過(guò)期4、緩存和數(shù)據(jù)庫(kù)間數(shù)據(jù)一致性問(wèn)題
分布式環(huán)境下(單機(jī)就不用說(shuō)了)非常容易出現(xiàn)緩存和數(shù)據(jù)庫(kù)間的數(shù)據(jù)一致性問(wèn)題,針對(duì)這一點(diǎn)的話,只能說(shuō),如果你的項(xiàng)目對(duì)緩存的要求是強(qiáng)一致性的,那么請(qǐng)不要使用緩存。我們只能采取合適的策略來(lái)降低緩存和數(shù)據(jù)庫(kù)間數(shù)據(jù)不一致的概率,而無(wú)法保證兩者間的強(qiáng)一致性。合適的策略包括 合適的緩存更新策略,更新數(shù)據(jù)庫(kù)后要及時(shí)更新緩存、緩存失敗時(shí)增加重試機(jī)制,例如MQ模式的消息隊(duì)列。
5、拓展
http://www.mobabel.net/總結(jié)redis熱點(diǎn)key發(fā)現(xiàn)及常見(jiàn)解決方案/
總結(jié)
以上是生活随笔為你收集整理的Redis缓存雪崩、缓存穿透、热点Key的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
 
                            
                        - 上一篇: 小米绿豆粥的功效与作用、禁忌和食用方法
- 下一篇: 第k个排列
