聊聊db和緩存一致性的5種實現方式


數據存儲在數據庫中,為了加快業務訪問的速度,我們將數據庫中的一些數據放在緩存中,那么問題來了,如何確保db和緩存中數據的一致性呢?我們列出了5種方法,大家都了解一下,然后根據業務自己選擇。

方案1

獲取緩存邏輯

使用過定時器,定時刷新redis中的緩存。

db更新數據邏輯

更新數據不用考慮緩存中的數據,直接更新數據就可以了

存在的問題

緩存中數據和db中數據一致性可能沒有那么及時,不過最終在某個時間點,數據是一致的。

方案2

獲取緩存邏輯

c1:根據key在redis中獲取對應的value

c2:如果value存在,直接返回value;若value不存在,繼續下面步驟

c3:從數據庫獲取值,賦值給value,然后將key->value放入redis,返回value

更新db邏輯

u1:開始db事務

u2:更新數據

u3:提交db事務

u4:刪除redis中當前數據的緩存

存在的問題

  1. 上面u3成功,u4失敗,會導致db數據更新成功,緩存刪除失敗,結果:db和緩存數據不一致
  2. 如果同時有很多線程到達c2發現緩存不存在,同時請求c3訪問db,會對db造成很大的壓力

方案3

獲取緩存邏輯

c1:根據key在redis中獲取對應的value

c2:如果value存在,直接返回value;若value不存在,繼續下面步驟

c3:從數據庫獲取值,賦值給value,然后將key->value放入redis,返回value

更新db邏輯

u1:刪除redis中當前數據的緩存

u2:開始db事務

u3:更新數據

u4:提交db事務

存在的問題

  1. 更新數據的線程執行u1成功之后,u2還未執行時,此時獲取緩存的線程剛好執行了c1到c3的邏輯,此時會將舊的數據放入redis,結果:db和緩存數據不一致
  2. 同樣存在方案2中說到的問題:如果同時有很多線程到達c2發現緩存不存在,同時請求c3訪問db,會對db造成很大的壓力

方案4

對方案2做改進,確保db更新成功之后,刪除緩存操作一定會執行,我們可以通過可靠消息來實現,可靠消息可以確保更新db操作和刪除redis中緩存最終要么都成功要么都失敗,依靠的是最終一致性來實現的。

改進之后過程如下。

獲取緩存邏輯

c1:根據key在redis中獲取對應的value

c2:如果value存在,直接返回value;若value不存在,繼續下面步驟

c3:從數據庫獲取值,賦值給value,然后將key->value放入redis,返回value

更新db邏輯

u1:開始db事務

u2:更新數據

u3:投遞刪除redis緩存的消息

u4:提交db事務

消息消費者-清理redis緩存的消費者

接受到清理redis緩存的消息之后,將redis中對應的緩存清除。

存在的問題

  1. 更新db和清理redis中的緩存之間存在一定的時間延遲,這段時間內,redis緩存的數據是舊的,也就是說這段時間內db和緩存數據是不一致的,但是最終會一致,這個不一致的時間可能比較小(這個需要看消息消費的效率了)
  2. 同樣存在方案2中說到的問題:如果同時有很多線程到達c2發現緩存不存在,同時請求c3訪問db,會對db造成很大的壓力

關於可靠消息的,可以看

方式5

我們先了解一些知識。

redis中幾個方法

get(key)

獲取key的值,如果存在,則返回;如果不存在,則返回nil

setnx(key,value)

setnx的含義就是SET if Not Exists,該方法是原子的,如果key不存在,則設置當前key成功,返回1;如果當前key已經存在,則設置當前key失敗,返回0

del(key)

將key對應的值從redis中刪除

數據庫相關知識

select v from t where t.key = #key# for update;

update t set v = #v# where t.key = #key#;

上面兩個sql會相互阻塞,直到其中一個提交之后,另外一個才可以繼續執行。

下面我們就通過上面的知識來實現db和緩存強一致性。

更新數據邏輯

1.打開db事務
2.update t set v = #v# where t.key = #key#;
3.根據key刪除redis中的緩存:RedisUti.del(key);
4.提交db事務

獲取緩存邏輯

/*公眾號:路人甲Java
* 工作10年的前阿里P7分享Java、算法、數據庫方面的技術干貨!
* 堅信用技術改變命運,讓家人過上更體面的生活。*/
public class CacheUtil {

    //根據key獲取緩存中對應的value
    public static String getCache(String key) throws InterruptedException {
        String value = RedisUtils.get(key);
        if (value != null) {
            return value;
        }
        //過期時間為當前時間+5秒
        String expireTimeKey = key + "ExpireTime";
        long expireTimeValue = System.currentTimeMillis() + 5000;
        //setnx是原子操作,所以只有一個會成功
        int setnx = RedisUtils.setnx(expireTimeKey, expireTimeValue + "");
        if (setnx == 0) {
            expireTimeValue = Long.valueOf(RedisUtils.get(expireTimeKey));
            //如果expireTimeValue小於當前時間,說明expireTimeKey過期了,將其刪除
            if (System.currentTimeMillis() > expireTimeValue) {
                //將expireTimeKey對應的刪除
                RedisUtils.del(expireTimeKey);
            } else {
                //休眠1秒繼續獲取
                TimeUnit.SECONDS.sleep(1);
            }
            //重試
            return getCache(key);
        } else {
            //1. 開啟db事務
            start transaction;
            //2. 執行select v from t where t.key = #key# for update; 將v的值賦值給value
            select v from t where t.key = #key# for update;
            RedisUtils.set(key, value);
            //3.提交db事務
            commit transaction;
        }
        return value;
    }

    //redis工具類,內部方法為偽代碼
    public static class RedisUtils {
        //根據key獲取value
        public static String get(String key) {
            return null;
        }

        //設置key對應的value
        public static void set(String key, String value) {
        }

        //刪除redis中一個key對應的值
        public static void del(String key) {
        }

        //setnx的含義就是SET if Not Exists,該方法是原子的,如果key不存在,
        //則設置當前key成功,返回1;如果當前key已經存在,則設置當前key失敗,返回0
        public static int setnx(String key, String value) {
            return 1;
        }
    }
}

這種方式可以確保db和redis中緩存同一時間強一致。

expireTimeKey為了防止某些線線程執行RedisUtils.setnx(expireTimeKey, expireTimeValue + "");返回1,表示setnx成功了,然后執行下一行代碼的時候系統后掛了,會導致將db數據加載到redis中失敗,代碼:if (System.currentTimeMillis() > expireTimeValue) 是給其他線程機會,可以獲取這個過期時間,發現過期之后直接刪掉,這樣其他線程才有機會將db數據load到redis中。

工作10年的前阿里P7分享Java、算法、數據庫方面的技術干貨!堅信用技術改變命運,讓家人過上更體面的生活!喜歡的請關注公眾號:路人甲Java


免責聲明!

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



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