緩存系列文章--5.緩存穿透問題

轉載請註明出處哈:http://carlosfu.iteye.com/blog/2269678

一. 緩存穿透 (請求數據緩存大量不命中):

    緩存穿透是指查詢一個一定不存在的數據,由於緩存不命中,並且出於容錯考慮, 如果從存儲層查不到數據則不寫入緩存,這將導致這個不存在的數據每次請求都要到存儲層去查詢,失去了緩存的意義。

    例如:下圖是一個比較典型的cache-storage架構,cache(例如memcache, redis等等) + storage(例如mysql, hbase等等)架構,查一個壓根就不存在的值, 如果不做兼容,永遠會查詢storage。

這裏寫圖片描述

二. 危害:

    對底層數據源(mysql, hbase, http接口, rpc調用等等)壓力過大,有些底層數據源不具備高併發性。

    例如mysql一般來說單臺能夠扛1000-QPS就已經很不錯了(別說你的查詢都是select * from table where id=xx 以及你的機器多麼牛逼,那就有點矯情了)

    例如他人提供的一個抗壓性很差的http接口,可能穿透會擊潰他的服務。

這裏寫圖片描述

三. 如何發現:

    我們可以分別記錄cache命中數, storage命中數,以及總調用量,如果發現空命中(cache,storage都沒有命中)較多,可能就會在緩存穿透問題。

    注意:緩存本身的命中率(例如redis中的info提供了類似數字,只代表緩存本身)不代表storage和業務的命中率。

四. 產生原因以及業務是否允許?

    產生原因有很多:可能是代碼本身或者數據存在的問題造成的,也很有可能是一些惡意攻擊、爬蟲等等(因爲http讀接口都是開放的)

    業務是否允許:這個要看做的項目或者業務是否允許這種情況發生,比如做一些非實時的推薦系統,假如新用戶來了,確實沒有他的推薦數據(推薦數據通常是根據歷史行爲算出),這種業務是會發生穿透現象的,至於業務允不允許要具體問題具體分析了。

五. 解決方法:

    解決思路大致有兩個,如下表。下面將分別說明

解決緩存穿透 適用場景 維護成本
緩存空對象 1.數據命中不高
2. 數據頻繁變化實時性高
1.代碼維護簡單
2.需要過多的緩存空間
3. 數據不一致
bloomfilter或者壓縮filter提前攔截 數據命中不高
2. 數據相對固定實時性低
1.代碼維護複雜
2.緩存空間佔用少
    1. 緩存空對象

這裏寫圖片描述

  1. 定義:如上圖所示,當第②步MISS後,仍然將空對象保留到Cache中(可能是保留幾分鐘或者一段時間,具體問題具體分析),下次新的Request(同一個key)將會從Cache中獲取到數據,保護了後端的Storage。
  2. 適用場景:數據命中不高,數據頻繁變化實時性高(一些亂轉業務)
  3. 維護成本:代碼比較簡單,但是有兩個問題:

         第一是空值做了緩存,意味着緩存系統中存了更多的key-value,也就是需要更多空間(有人說空值沒多少,但是架不住多啊),解決方法是我們可以設置一個較短的過期時間。

         第二是數據會有一段時間窗口的不一致,假如,Cache設置了5分鐘過期,此時Storage確實有了這個數據的值,那此段時間就會出現數據不一致,解決方法是我們可以利用消息或者其他方式,清除掉Cache中的數據。
4. 僞代碼:

//JAVA
package com.carlosfu.service;  

import org.apache.commons.lang.StringUtils;  

import com.carlosfu.cache.Cache;  
import com.carlosfu.storage.Storage;  

/** 
 * 某服務 
 *  
 * @author carlosfu 
 * @Date 2015-10-11 
 * @Time 下午6:28:46 
 */  
public class XXXService {  

    /** 
     * 緩存 
     */  
    private Cache cache = new Cache();  

    /** 
     * 存儲 
     */  
    private Storage storage = new Storage();  

    /** 
     * 模擬正常模式 
     * @param key 
     * @return 
     */  
    public String getNormal(String key) {  
        // 從緩存中獲取數據  
        String cacheValue = cache.get(key);  
        // 緩存爲空  
        if (StringUtils.isBlank(cacheValue)) {  
            // 從存儲中獲取  
            String storageValue = storage.get(key);  
            // 如果存儲數據不爲空,將存儲的值設置到緩存  
            if (StringUtils.isNotBlank(storageValue)) {  
                cache.set(key, storageValue);  
            }  
            return storageValue;  
        } else {  
            // 緩存非空  
            return cacheValue;  
        }  
    }  


    /** 
     * 模擬防穿透模式 
     * @param key 
     * @return 
     */  
    public String getPassThrough(String key) {  
        // 從緩存中獲取數據  
        String cacheValue = cache.get(key);  
        // 緩存爲空  
        if (StringUtils.isBlank(cacheValue)) {  
            // 從存儲中獲取  
            String storageValue = storage.get(key);  
            cache.set(key, storageValue);  
            // 如果存儲數據爲空,需要設置一個過期時間(300秒)  
            if (StringUtils.isBlank(storageValue)) {  
                cache.expire(key, 60 * 5);  
            }  
            return storageValue;  
        } else {  
            // 緩存非空  
            return cacheValue;  
        }  
    }  

}  
    2. bloomfilter或者壓縮filter(bitmap等等)提前攔截

這裏寫圖片描述

  1. 定義:如上圖所示,在訪問所有資源(cache, storage)之前,將存在的key用布隆過濾器提前保存起來,做第一層攔截, 例如: 我們的推薦服務有4億個用戶uid, 我們會根據用戶的歷史行爲進行推薦(非實時),所有的用戶推薦數據放到hbase中,但是每天有許多新用戶來到網站,這些用戶在當天的訪問就會穿透到hbase。爲此我們每天4點對所有uid做一份布隆過濾器。如果布隆過濾器認爲uid不存在,那麼就不會訪問hbase,在一定程度保護了hbase(減少30%左右)。

    注:有關布隆過濾器的相關知識,請自行查閱,有關guava中如何使用布隆過濾器,之後會系列文章給大家介紹。

  2. 適用場景:數據命中不高,數據相對固定實時性低(通常是數據集較大)

  3. 維護成本:代碼維護複雜, 緩存空間佔用少

        第一是空值做了緩存,意味着緩存系統中存了更多的key-value,也就是需要更多空間(有人說空值沒多少,但是架不住多啊),解決方法是我們可以設置一個較短的過期時間。

         第二是數據會有一段時間窗口的不一致,假如,Cache設置了5分鐘過期,此時Storage確實有了這個數據的值,那此段時間就會出現數據不一致,解決方法是我們可以利用消息或者其他方式,清除掉Cache中的數據。

六、參考資料:

http://www.xwuxin.com/?p=1938
http://blog.jobbole.com/83439/ (那些年我們一起追過的緩存寫法)
http://www.tuicool.com/articles/7jMZFzj

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章