1. 程式人生 > 其它 >Redis(九)快取穿透、雪崩、擊穿

Redis(九)快取穿透、雪崩、擊穿

前言

作為一種非關係型資料庫,redis也總是免不了有各種各樣的問題,這篇文章主要是針對其中三個問題進行講解:快取穿透、快取擊穿和快取雪崩,並給出一些解決方案

快取穿透

快取穿透是指查詢一個一定不存在的資料,由於快取是不命中時被動寫的,並且出於容錯考慮,如果從儲存層查不到資料則不寫入快取,這將導致這個不存在的資料每次請求都要到儲存層去查詢,失去了快取的意義。在流量大時,可能DB就掛掉了,要是有人利用不存在的key頻繁攻擊我們的應用,這就是漏洞

小點的單機系統,基本上用postman就能搞死。

像這種你如果不對引數做校驗,資料庫id都是大於0的,我一直用小於0的引數去請求你,每次都能繞開Redis直接打到資料庫,資料庫也查不到,每次都這樣,併發高點就容易崩掉了

快取穿透解決方案

有很多種方法可以有效地解決快取穿透問題。

1.最常見的則是採用布隆過濾器,它是一種資料結構,將所有可能存在的資料雜湊到一個足夠大的bitmap中,一個一定不存在的資料會被 這個bitmap攔截掉,從而避免了對底層儲存系統的查詢壓力。

2.另外也有一個更為簡單粗暴的方法(我們採用的就是這種),如果一個查詢返回的資料為空(不管是數 據不存在,還是系統故障),我們仍然把這個空結果進行快取,但它的過期時間會很短,最長不超過五分鐘。

快取雪崩

我瞭解的,目前電商首頁以及熱點資料都會去做快取 ,一般快取都是定時任務去重新整理,或者是查不到之後去更新的,定時任務重新整理就有一個問題。

舉個簡單的例子:如果所有首頁的Key失效時間都是12小時,中午12點重新整理的,我零點有個秒殺活動大量使用者湧入,假設當時每秒 6000 個請求,本來快取在可以扛住每秒 5000 個請求,但是快取當時所有的Key都失效了。此時 1 秒 6000 個請求全部落資料庫,資料庫必然扛不住,它會報一下警,真實情況可能DBA都沒反應過來就直接掛了。此時,如果沒用什麼特別的方案來處理這個故障,DBA 很著急,重啟資料庫,但是資料庫立馬又被新的流量給打死了。這就是我理解的快取雪崩

一般的專案再吊的都不允許這麼大的QPS直接打DB去,不過沒慢SQL加上分庫,大表分表可能還還算能頂,但是跟用了Redis的差距還是很大

同一時間大面積失效,那一瞬間Redis跟沒有一樣,那這個數量級別的請求直接打到資料庫幾乎是災難性的,你想想如果打掛的是一個使用者服務的庫,那其他依賴他的庫所有的介面幾乎都會報錯,如果沒做熔斷等策略基本上就是瞬間掛一片的節奏,你怎麼重啟使用者都會把你打掛,等你能重啟的時候,使用者早就睡覺去了,並且對你的產品失去了信心,什麼垃圾產品。

快取雪崩解決方案

處理快取雪崩簡單,在批量往Redis存資料的時候,把每個Key的失效時間都加個隨機值就好了,這樣可以保證資料不會在同一時間大面積失效了!

快取擊穿

為什麼把快取擊穿拿到最後說,因為它最複雜也最難處理,解決方案也有很多種,大家要仔細看哦!

出現快取擊穿有以下這些可能

  1. 這個跟快取雪崩有點像,但是又有一點不一樣,快取雪崩是因為大面積的快取失效,打崩了DB,而快取擊穿不同的是快取擊穿是指一個Key非常熱點,在不停的扛著大併發,大併發集中對這一個點進行訪問,當這個Key在失效的瞬間,持續的大併發就穿破快取,直接請求資料庫,就像在一個完好無損的桶上鑿開了一個洞。

  2. 就是這個值是資料庫新增的,但是快取中暫時還沒有,這個時候剛好併發請求進來了,如果處理不當也會發生

快取擊穿解決方案

我們的目標是:儘量少的執行緒構建快取(甚至是一個) + 資料一致性 + 較少的潛在危險,下面會介紹四種方法來解決這個問題:

一、使用互斥鎖(mutex key)

業界比較常用的做法,是使用mutex。簡單地來說,就是在快取失效的時候(判斷拿出來的值為空),不是立即去load db,而是先使用快取工具的某些帶成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一個mutex key,當操作返回成功時,再進行load db的操作並回設快取;否則,就重試整個get快取的方法。

String get(String key) {  
   String value = redis.get(key);  
   if (value  == null) {  
    if (redis.setnx(key_mutex, "1")) {  
        // 3 min timeout to avoid mutex holder crash  
        redis.expire(key_mutex, 3 * 60)  
        value = db.get(key);  
        redis.set(key, value);  
        redis.delete(key_mutex);  
    } else {  
        //其他執行緒休息50毫秒後重試  
        Thread.sleep(50);  
        get(key);  
    }  
  }  
}  

二、"提前"使用互斥鎖(mutex key)

在value內部設定1個超時值(timeout1), timeout1比實際的memcache timeout(timeout2)小。當從cache讀取到timeout1發現它已經過期時候,馬上延長timeout1並重新設定到cache。然後再從資料庫載入資料並設定到cache中。虛擬碼如下:

v = rediscache.get(key);  
if (v == null) {  
    if (rediscache.setnx(key_mutex, 3 * 60 * 1000) == true) {  
        value = db.get(key);  
        rediscache.set(key, value);  
        rediscache.delete(key_mutex);  
    } else {  
        sleep(50);  
        retry();  
    }  
} else {  
    if (v.timeout <= now()) {  
        if (rediscache.setnx(key_mutex, 3 * 60 * 1000) == true) {  
            // extend the timeout for other threads  
            v.timeout += 3 * 60 * 1000;  
            rediscache.set(key, v, KEY_TIMEOUT * 2);  
            // load the latest value from db  
            v = db.get(key);  
            v.timeout = KEY_TIMEOUT;  
            rediscache.set(key, value, KEY_TIMEOUT * 2);  
            rediscache.delete(key_mutex);  
        } else {  
            sleep(50);  
            retry();  
        }  
    }  

三、設定永不過期

這裡的“永遠不過期”包含兩層意思:

  1. 從redis上看,確實沒有設定過期時間,這就保證了,不會出現熱點key過期問題,也就是“物理”不過期
  2. 從功能上看,如果不過期,那不就成靜態的了嗎?所以我們把過期時間存在key對應的value裡,如果發現要過期了,通過一個後臺的非同步執行緒進行快取的構建,也就是“邏輯”過期

從實戰看,這種方法對於效能非常友好,唯一不足的就是構建快取時候,其餘執行緒(非構建快取的執行緒)可能訪問的是老資料,但是對於一般的網際網路功能來說這個還是可以忍受。

String get(final String key) {  
        V v = redis.get(key);  
        String value = v.getValue();  
        long timeout = v.getTimeout();  
        if (v.timeout <= System.currentTimeMillis()) {  
            // 非同步更新後臺異常執行  
            threadPool.execute(new Runnable() {  
                public void run() {  
                    String keyMutex = "mutex:" + key;  
                    if (redis.setnx(keyMutex, "1")) {  
                        // 3 min timeout to avoid mutex holder crash  
                        redis.expire(keyMutex, 3 * 60);  
                        String dbValue = db.get(key);  
                        redis.set(key, dbValue);  
                        redis.delete(keyMutex);  
                    }  
                }  
            });  
        }  
        return value;  
    }  

四、資源保護

採用netflix的hystrix,可以做資源的隔離保護主執行緒池,如果把這個應用到快取的構建也未嘗不可。

方案對比

作為一個併發量較大的網際網路應用,我們的目標有3個:

  1. 加快使用者訪問速度,提高使用者體驗
  2. 降低後端負載,保證系統平穩
  3. 保證資料“儘可能”及時更新(要不要完全一致,取決於業務,而不是技術)

所以第二節中提到的四種方法,可以做如下比較,還是那就話:沒有最好,只有最合適

解決方案 優點 缺點
簡單分散式鎖(Tim yang)

1. 思路簡單

2. 保證一致性

1. 程式碼複雜度增大

2. 存在死鎖的風險

3. 存線上程池阻塞的風險

加另外一個過期時間(Tim yang) 1. 保證一致性 同上
不過期(本文)

1. 非同步構建快取,不會阻塞執行緒池

1. 不保證一致性。

2. 程式碼複雜度增大(每個value都要維護一個timekey)。

3. 佔用一定的記憶體空間(每個value都要維護一個timekey)。

資源隔離元件hystrix(本文)

1. hystrix技術成熟,有效保證後端。

2. hystrix監控強大。

1. 部分訪問存在降級策略。

當然在請求剛進來的時候,也需要做好多處理:

在介面層增加校驗,比如使用者鑑權校驗,引數做校驗,不合法的引數直接程式碼Return,比如:id 做基礎校驗,id <=0的直接攔截等。

總結

本文簡單的介紹了,Redis的雪崩,擊穿,穿透,三者其實都差不多,但是又有一些區別,在面試中其實這是問到快取必問的,大家不要把三者搞混了,因為快取雪崩、穿透和擊穿,是快取最大的問題,要麼不出現,一旦出現就是致命性的問題,所以面試官一定會問你。

大家一定要理解是怎麼發生的,以及是怎麼去避免的,發生之後又怎麼去搶救,你可以不是知道很深入,但是你不能一點都不去想,面試有時候不一定是對知識面的拷問,或許是對你的態度的拷問,如果你思路清晰,然後知其然還知其所以然那就很贊,還知道怎麼預防那肯定可以過五關斬六將。