緩存與數據庫讀寫模式
- 讀數據時先讀緩存,如果緩存中有數據直接響應,如果沒有數據,查詢數據庫,寫入緩存,同時響應
- 寫數據時,先寫入數據庫,並刪除緩存
爲什麼寫數據時刪除緩存而不是更新緩存?
如果這個數據寫多讀少,頻繁的更新緩存反而會造成資源浪費,刪除緩存則採用了懶加載的思想。
緩存失效模式存在的問題
如果多個線程同時在緩存過期時訪問數據庫請求數據,由於種種狀況的可能(比如一個線程先訪問到數據庫,但是在取數據時卡住了,另一個線程訪問到數據庫,取到數據放入緩存,這時前一個線程剛取到數據,又把更新的緩存給覆蓋掉了),可能導致緩存和數據庫數據不一致的問題。
如何保證緩存與數據庫數據一致性
核心是在緩存過期時,所有微服務只能有一個請求訪問數據庫。
另外,如果數據的一致性要求不是非常嚴格,即使在短時間內存在數據不一致的情況,只要設置了緩存過期時間,每隔一段時間會自動更新到最新數據,不影響實際業務也可。
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框架已經足夠了,同時註解可以大大簡化開發。