Redis_保證緩存與數據庫數據一致性/分佈式鎖/預防緩存擊穿/Spring Cache能否保證數據一致性

緩存與數據庫讀寫模式

  • 讀數據時先讀緩存,如果緩存中有數據直接響應,如果沒有數據,查詢數據庫,寫入緩存,同時響應
  • 寫數據時,先寫入數據庫,並刪除緩存

爲什麼寫數據時刪除緩存而不是更新緩存?

如果這個數據寫多讀少,頻繁的更新緩存反而會造成資源浪費,刪除緩存則採用了懶加載的思想。

緩存失效模式存在的問題

如果多個線程同時在緩存過期時訪問數據庫請求數據,由於種種狀況的可能(比如一個線程先訪問到數據庫,但是在取數據時卡住了,另一個線程訪問到數據庫,取到數據放入緩存,這時前一個線程剛取到數據,又把更新的緩存給覆蓋掉了),可能導致緩存和數據庫數據不一致的問題。

如何保證緩存與數據庫數據一致性

核心是在緩存過期時,所有微服務只能有一個請求訪問數據庫。

另外,如果數據的一致性要求不是非常嚴格,即使在短時間內存在數據不一致的情況,只要設置了緩存過期時間,每隔一段時間會自動更新到最新數據,不影響實際業務也可。

Redisson分佈式鎖

public Map<String, List<Vo>> getJsonFromDBWithRedissonLock() {
    // 1、佔分布式鎖,去reids佔坑
    RLock lock = redisson.getLock("Json-lock");
    lock.lock(); // 阻塞等待
    // 加鎖成功...執行業務
    Map<String, List<Vo>> dataFromDB;
    try {
        // 訪問數據庫
        dataFromDB = getJSONDataFromDB();
    } finally {
        lock.unlock(); // 解鎖
    }
    return dataFromDB;
}

Redisson的加鎖方式和JUC中Lock很像。

  • 鎖自動續期,30s,每10s續期,不必擔心業務時間過長,鎖過期自動刪除
  • 構造器自行指定解鎖時間,將不會自動續期
  • 建議使用手動指定過期時間,手動釋放鎖

Redis分佈式鎖

Redisson其實就是封裝了Redis的分佈式鎖,Redis指令實現分佈式鎖主要是運用Set NX EX命令

public Map<String, List<Vo>> getJsonFromDBWithRedisLock() {
    // 搶佔分佈式鎖 setIfAbsent --> NX 不存在才佔坑 EX 自動過期時間
    // 設置redis鎖的自動過期時間 - 防止出現異常、服務崩塌等各種情況,沒有執行刪除鎖操作導致的死鎖問題
    // 設置過期時間和加鎖必須是同步的、原子的
    String uuid = UUID.randomUUID().toString();
    Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
    if (lock) {
        System.out.println("獲取分佈式鎖成功...");
        // 加鎖成功...執行業務
        Map<String, List<Vo>> dataFromDB;
        try {
            // 訪問數據庫
            dataFromDB = getJSONDataFromDB();
        } finally {
            // 獲取對比值和對比成功刪除鎖也是要同步的、原子的執行  
            // 參照官方使用lua腳本解鎖
            String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";

            Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
                    Arrays.asList("lock"), uuid);
        }
        return dataFromDB;
    } else {
        // 加鎖失敗休眠一段時間...重試獲取鎖
        System.out.println("獲取分佈式鎖失敗...等待重試");
        // 重試的頻率太快會導致內存溢出
        try {
            Thread.sleep(200);
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 自旋的方式
        return getJsonFromDBWithRedisLock();
    }
}

緩存擊穿是什麼?

緩存中的熱點key過期,剛好高併發情況下,大量請求訪問該key,由於緩存中沒有,所以請求直接打到數據庫,造成數據庫壓力激增。

如何解決?

  • 分佈式鎖,保證只有一個請求訪問數據庫,其他請求阻塞等待
  • 將熱點key過期時間延長,或暫時設置爲永久key

Spring Cache提供的緩存框架能否保證緩存與數據庫數據一致性?

一定程度上保證。

 @Cacheable(value = {"value"}, key = "#root.methodName", sync = true)

通過將sync設置爲true,在訪問數據庫放入緩存的方法是用synchronized修飾的,

public synchronized <T> T get(Object key, Callable<T> valueLoader) {
	//首先從緩存中嘗試獲取值
    ValueWrapper result = this.get(key);
    //如果有結果直接返回
    if (result != null) {
        return result.get();
    } else {
    	//如果沒有查詢數據庫
        T value = valueFromLoader(key, valueLoader);
        //放入緩存
        this.put(key, value);
        //返回結果
        return value;
    }
}

這在單個微服務中是可以保證的,但是在微服務集羣中是保證不了的,因爲synchronized只能鎖this,也就是說加入有10個相同業務的微服務,在某個緩存失效後,最不理想的情況下每個微服務會有1個請求訪問到數據庫,也就是有10個請求訪問到數據庫。一般來說,這樣設置並不會造成數據庫的壓力激增,所以一般使用Spring Cache框架已經足夠了,同時註解可以大大簡化開發。

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