redis緩存一致性


redis緩存一致性

redis是目前使用最廣泛的分布式緩存系統,幾乎每家公司都在用。它使用簡單,吞吐量高,單機 qps 可以達到 10 萬每秒,但在使用redis緩存時存在一個問題,即如何保證緩存數據和數據庫中數據的一致性。本文就一致性問題提出常用的解決方案。

一致性問題

讀取流程

首先,讀緩存;

如果緩存里沒有值,那就讀取數據庫的值;

同時把這個值寫進緩存中。

雙更模式

先更新緩存,再更新數據庫

public void putValue(key, value){
    putToRedis(key, value);
    putToDB(key, value);//異常回滾
}

比如更新一個值,首先刷了緩存,然后把數據庫也更新了。但過程中,更新數據庫可能會失敗,發生了回滾。所以,最后“緩存里的數據”和“數據庫的數據”就不一樣了,也就是出現了數據一致性問題。

先更新數據庫,再更新緩存

public void putValue(key, value){
    // 先更新庫
    putToDB(key, value);
    // 再更新緩存
    putToRedis(key, value);
}

問題:操作 A 更新 a 的值為 1,操作 B 更新 a 的值為 2。由於數據庫和 Redis 的操作,並不是原子的,它們的執行時長也不是可控制的。當兩個請求的時序發生了錯亂,就會發生緩存不一致的情況。

雙更模式下,數據不一致的概率較大,一般不建議使用雙更模式。

刪除模式

刪除模式即更新數據時,刪除緩存,查詢時重新從數據庫中加載數據。先刪除緩存還是后刪除緩存?

先刪除緩存

public void putValue(key, value){
    deleteFromRedis(key);
    putToDB(key,value);
}

問題:請求A刪除了某個 key 的值,這時候有另外一個請求B 到來,那么它就會擊穿到數據庫,讀取到舊的值。無論操作A更新數據庫的操作持續多長時間,都會產生不一致的情況。

后刪除緩存(Cache-Aside Pattern)

后刪除緩存不會出現上述問題。一般情況下這種方式可以解決大部分問題,也是最常用的解決方案。

但是在高並發的情況下,仍有可能出現不一致的情況。場景如下:

public void proccess(key, value){
    N:putToDB(key, 1);
    N:deleteFromRedis(key);
    // A B線程同時操作同一組數據
    A:getFromRedis(key);
    A:getFromDB(key)=1;

    B:putToDB(key, 2);
    B:deleteFromRedis(key);
    // 特殊情況下導致A更新redis慢於B,在B刪除redis之后A才完成更新
    A:putToRedis(key, 1);
    
    //DB=2,Redis=1
}

有一系列的高並發操作,一直執行着更新、刪除的動作。某個時刻,它更新數據庫的值為 1,然后刪除了緩存。

正在這時,有兩個請求發生了:

  • 一個是讀操作,讀到的當然是數據庫的舊值 1,我們記作操作 A;
  • 同時,另外一個請求發起了更新操作,把數據庫記錄更新為 2,我們記作操作 B。

一般情況下,讀取操作都是比寫入操作快的,但我們要考慮兩種極端情況:

  • 一種是這個讀取操作 A,發生在更新操作 B 的尾部;
  • 一種是操作 A 的這個 Redis 的操作時長,耗費了非常多的時間。比如,這個節點正好發生了 STW。(條件比較苛刻)

那么很容易地,讀操作 A 的結束時間就超過了操作 B 刪除的動作。

實際上,你也無法控制它們的執行順序。只要發生這種情況,大概率數據庫和Redis的值會不一致。

此種場景下如何解決?

延遲雙刪

如果有一種機制,能夠確保刪除動作一定被執行,那就可以解決問題,至少能縮小數據不一致的時間。常用的方法就是延時雙刪,依然是先更新再刪除,唯一不同的是:我們把這個刪除動作,在不久之后再執行一次,比如 5 秒之后。

public void putValue(key, value){
    putToDB(key, value);
    deleteFromRedis(key);
    // 5秒之后再次進行刪除
    deleteFromRedisDelay(key, 5second);
}

延遲刪除動作也有多種實現方式:

  • 如果放在DelayQueue中,會有隨着 JVM 進程的死亡,丟失更新的風險;
  • 如果放在 MQ 中,會增加編碼的復雜性。

實現方案要根據實際情況進行選擇,沒有完美的方案,只要能滿足業務需求即可。

設置較小的緩存時間

俗稱閃電緩存,即把緩存的失效時間設置非常短,比如 5秒。一旦失效,就會再次去數據庫讀取最新數據到緩存,即數據不一致只會在短時間內不一致。但這種方式,在非常高的並發下,同一時間對某個 key 的請求擊穿到 DB,產生緩存擊穿問題。

緩存擊穿

緩存擊穿,指的是緩存中沒有數據但數據庫中有,由於同一時刻請求量特別大,但是沒有讀到緩存數據,就會一股腦涌入到數據庫中讀取,導致數據庫因壓力過大不可用。

解決方案:

  • 讀操作互斥,使用鎖或者分布式鎖來控制;
  • 更新集中,采用定時或者 binlog 的方式同步更新。
getValue(key){
    res = getFromRedis(key);
    // 未命中
    if(null == res){
        lock.lock(...);
        // 再次讀取緩存為null
        res = getFromRedis(key);
        if(res == null){
            res = getFromDB(key);
            if(null != res){
                putToRedis(key,res);
            }
        }
        lock.unlock();
    }
    return res;
}



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM