應對緩存擊穿的解決方法

一.什麼樣的數據適合緩存?
分析一個數據是否適合緩存,我們要從訪問頻率、讀寫比例、數據一致性等要求去分析. 


二.什麼是緩存擊穿
在高併發下,多線程同時查詢同一個資源,如果緩存中沒有這個資源,那麼這些線程都會去數據庫查找,對數據庫造成極大壓力,緩存失去存在的意義.打個比方,數據庫是人,緩存是防彈衣,子彈是線程,本來防彈衣是防止子彈打到人身上的,但是當防彈衣裏面沒有防彈的物質時,子彈就會穿過它打到人身上. 


三.緩存擊穿的解決辦法
方案一
後臺刷新

後臺定義一個job(定時任務)專門主動更新緩存數據.比如,一個緩存中的數據過期時間是30分鐘,那麼job每隔29分鐘定時刷新數據(將從數據庫中查到的數據更新到緩存中).

這種方案比較容易理解,但會增加系統複雜度。比較適合那些 key 相對固定,cache 粒度較大的業務,key 比較分散的則不太適合,實現起來也比較複雜。
方案二
檢查更新

將緩存key的過期時間(絕對時間)一起保存到緩存中(可以拼接,可以添加新字段,可以採用單獨的key保存..不管用什麼方式,只要兩者建立好關聯關係就行).在每次執行get操作後,都將get出來的緩存過期時間與當前系統時間做一個對比,如果緩存過期時間-當前系統時間<=1分鐘(自定義的一個值),則主動更新緩存.這樣就能保證緩存中的數據始終是最新的(和方案一一樣,讓數據不過期.)

這種方案在特殊情況下也會有問題。假設緩存過期時間是12:00,而 11:59 
到 12:00這 1 分鐘時間裏恰好沒有 get 請求過來,又恰好請求都在 11:30 分的時 
候高並發過來,那就悲劇了。這種情況比較極端,但並不是沒有可能。因爲“高 
併發”也可能是階段性在某個時間點爆發。
方案三
分級緩存

採用 L1 (一級緩存)和 L2(二級緩存) 緩存方式,L1 緩存失效時間短,L2 緩存失效時間長。 請求優先從 L1 緩存獲取數據,如果 L1緩存未命中則加鎖,只有 1 個線程獲取到鎖,這個線程再從數據庫中讀取數據並將數據再更新到到 L1 緩存和 L2 緩存中,而其他線程依舊從 L2 緩存獲取數據並返回。

這種方式,主要是通過避免緩存同時失效並結合鎖機制實現。所以,當數據更 
新時,只能淘汰 L1 緩存,不能同時將 L1 和 L2 中的緩存同時淘汰。L2 緩存中 
可能會存在髒數據,需要業務能夠容忍這種短時間的不一致。而且,這種方案 
可能會造成額外的緩存空間浪費。
方案四
加鎖

方法1
    // 方法1:
    public synchronized List<String> getData01() {
        List<String> result = new ArrayList<String>();
        // 從緩存讀取數據
        result = getDataFromCache();
        if (result.isEmpty()) {
            // 從數據庫查詢數據
            result = getDataFromDB();
            // 將查詢到的數據寫入緩存
            setDataToCache(result);
        }
        return result;
    }

這種方式確實能夠防止緩存失效時高併發到數據庫,但是緩存沒有失效的時候,在從緩存中拿數據時需要排隊取鎖,這必然會大大的降低了系統的吞吐量.
方法2
// 方法2:
    static Object lock = new Object();

    public List<String> getData02() {
        List<String> result = new ArrayList<String>();
        // 從緩存讀取數據
        result = getDataFromCache();
        if (result.isEmpty()) {
            synchronized (lock) {
                // 從數據庫查詢數據
                result = getDataFromDB();
                // 將查詢到的數據寫入緩存
                setDataToCache(result);
            }
        }
        return result;
    }

這個方法在緩存命中的時候,系統的吞吐量不會受影響,但是當緩存失效時,請求還是會打到數據庫,只不過不是高併發而是阻塞而已.但是,這樣會造成用戶體驗不佳,並且還給數據庫帶來額外壓力.
方法3
//方法3
    public List<String> getData03() {
        List<String> result = new ArrayList<String>();
        // 從緩存讀取數據
        result = getDataFromCache();
        if (result.isEmpty()) {
            synchronized (lock) {
            //雙重判斷,第二個以及之後的請求不必去找數據庫,直接命中緩存
                // 查詢緩存
                result = getDataFromCache();
                if (result.isEmpty()) {
                    // 從數據庫查詢數據
                    result = getDataFromDB();
                    // 將查詢到的數據寫入緩存
                    setDataToCache(result);
                }
            }
        }
        return result;
    }

雙重判斷雖然能夠阻止高併發請求打到數據庫,但是第二個以及之後的請求在命中緩存時,還是排隊進行的.比如,當30個請求一起並發過來,在雙重判斷時,第一個請求去數據庫查詢並更新緩存數據,剩下的29個請求則是依次排隊取緩存中取數據.請求排在後面的用戶的體驗會不爽.

方法4
static Lock reenLock = new ReentrantLock();

    public List<String> getData04() throws InterruptedException {
        List<String> result = new ArrayList<String>();
        // 從緩存讀取數據
        result = getDataFromCache();
        if (result.isEmpty()) {
            if (reenLock.tryLock()) {
                try {
                    System.out.println("我拿到鎖了,從DB獲取數據庫後寫入緩存");
                    // 從數據庫查詢數據
                    result = getDataFromDB();
                    // 將查詢到的數據寫入緩存
                    setDataToCache(result);
                } finally {
                    reenLock.unlock();// 釋放鎖
                }

            } else {
                result = getDataFromCache();// 先查一下緩存
                if (result.isEmpty()) {
                    System.out.println("我沒拿到鎖,緩存也沒數據,先小憩一下");
                    Thread.sleep(100);// 小憩一會兒
                    return getData04();// 重試
                }
            }
        }
        return result;
    }

最後使用互斥鎖的方式來實現,可以有效避免前面幾種問題.
當然,在實際分佈式場景中,我們還可以使用 redis、tair、zookeeper 等提供的分佈式鎖來實現.但是,如果我們的併發量如果只有幾千的話,何必殺雞焉用牛刀呢?
--------------------- 
作者:不善言談者 
來源:CSDN 
原文:https://blog.csdn.net/bushanyantanzhe/article/details/79459095 
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!

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