Redis緩存穿透、擊穿、雪崩,數據庫與緩存一致性

Redis作為高性能非關係型(NoSQL)的鍵值對數據庫,受到了廣大用戶的喜愛和使用,大家在項目中都用到了Redis來做數據緩存,但有些問題我們在使用中不得不考慮,其中典型的問題就是:緩存穿透緩存雪崩緩存擊穿與關係型數據庫的一致性

一、緩存穿透

1、概念

緩存穿透是指查詢一個緩存和數據庫不存在的數據。正常的使用緩存流程大致是,數據查詢先進行緩存查詢,如果key不存在或者key已經過期,再對數據庫進行查詢,並把查詢到的對象,放進緩存。如果數據庫查詢對象為空,則不放進緩存。

大致流程如下圖所示:

在一些特定場景 例如:秒殺活動。同一時刻會有大量的請求,都在秒殺同一件商品,這些請求同時去查緩存中沒有數據,然後又同時訪問數據庫。結果悲劇了,數據庫可能扛不住壓力,直接掛掉。

也會存在有人惡意請求。一般我們的主鍵ID都是無符號的自增類型,有些人想要搞垮你的數據庫,每次請求都用負數ID,而ID為負數的記錄在數據庫根本就沒有。就會每次都去查詢數據庫,而每次查詢都是空,每次又都不會進行緩存。假如有惡意攻擊,就可以利用這個漏洞,對數據庫造成壓力,甚至壓垮數據庫。

3、解決方案

1) 驗證攔截

接口層進行校驗,如鑒定用戶權限,對ID之類的字段做基礎的校驗,如id<=0的字段直接攔截。

2) 布隆過濾器

我們可以提前將真實正確的商品id,添加到過濾器當中,每次再進行查詢時,先確認要查詢的id是否在過濾器當中,如果不在,則說明id為非法id,則不需要進行後續的查詢步驟了。

布隆過濾器是一種比較獨特數據結構,有一定的誤差。布隆過濾器的特點就是 如果它說不存在那肯定不存在,如果它說存在,那數據有可能實際不存在

它最大的優點就是性能高,空間佔用率及小。

3) 緩存空對象

當存儲層不命中後,即使返回的空對象也將其緩存起來,同時會設置一個過期時間,之後再訪問這個數據將會從緩存中獲取,保護了後端數據源。

但是這種方法會存在兩個問題:

  • 如果空值能夠被緩存起來,這就意味着緩存需要更多的空間存儲更多的鍵,因為這當中可能會有很多的空值的鍵;
  • 即使對空值設置了過期時間,還是會存在緩存層和存儲層的數據會有一段時間窗口的不一致,這對於需要保持一致性的業務會有影響。

二、緩存擊穿

1、概念

緩存擊穿,是指緩存中沒有但數據庫中有的數據,並且某一個key非常熱點,在不停的扛着大並發,大並發集中對這一個點進行訪問,當這個key緩存時間到期,持續的大並發就穿破緩存,直接請求數據庫,導致壓垮數據庫。

2、解決方案

1)設置熱點數據永遠不過期。

這個方法就比較粗暴,,如果你的熱點數據要求實時性比較低,那麼可以設置熱點數據在熱點時段不過期,在訪問低峰期過期,比如每天凌晨過期。

2) 使用分佈式鎖

互斥鎖可以控制查詢數據庫的線程訪問,但這種方案會導致系統的吞吐量下降,需要根據實際情況使用。

public static String getData(String key) throws InterruptedException {
	//從Redis查詢數據 
	String result = getDataByKV(key);
	//參數校驗
	if (StringUtils.isBlank(result)) {
		try {
			//獲得鎖
			if (reenLock.tryLock()) {
				//去數據庫查詢 
				result = getDataByDB(key);
				//校驗
				if (StringUtils.isNotBlank(result)) {
				//插進緩存 
				setDataToKV(key, result); 
				}
			} else {
			//睡一會再拿 
			Thread.sleep(100L); 
			result = getData(key); 
			} 
		} finally {
		//釋放鎖 
		reenLock.unlock(); 
		} 
	}
return result; 
}

三、緩存雪崩

1、概念

緩存雪崩表示在某一時間段緩存集中失效,導致請求全部走數據庫,引起數據庫壓力過大甚至down機。和緩存擊穿不同的是,緩存擊穿指並發查同一條數據,緩存雪崩是不同數據都過期了,很多數據都查不到從而查數據庫。

使緩存集中失效的原因:

1)、redis服務器掛掉了。

Redis 集群產生了大面積故障;緩存失敗,此時仍有大量請求去訪問 Redis緩存服務器;在大量 Redis 請求失敗後,這些請求將會去訪問數據庫;

2、對緩存數據設置了相同的過期時間,導致某時間段內緩存集中失效。

2、解決方案

1)實現Redis的高可用

【事前】搭建Redis 哨兵(Sentinel) 或 Redis 集群(Cluster) 都可以做到高可用;

【事中】緩存降級(臨時支持):當訪問次數急劇增加導致服務出現問題時,我們如何確保服務仍然可用。在國內使用比較多的是 Hystrix,它通過熔斷、降級、限流三個手段來降低雪崩發生後的損失。只要確保數據庫不死,系統總可以響應請求。

【事後】Redis備份和快速預熱:Redis數據備份和恢復、快速緩存預熱。

2)緩存數據的過期時間設置隨機,防止同一時間大量數據過期現象發生。

(1)採取不同分類商品,緩存不同周期。在同一分類中的商品,加上一個隨機因子。這樣能儘可能分散緩存過期時間,而且,熱門類目的商品緩存時間長一些,冷門類目的商品緩存時間短一些,也能節省緩存服務的資源。

(2)如果緩存數據庫是分佈式部署,將 熱點數據均勻分佈在不同的緩存數據庫中。

(3)設置熱點數據永遠不過期

四、數據庫與緩存一致性

使用緩存,可以降低耗時,提供系統吞吐性能。但是,使用緩存,會存在數據一致性的問題。

1、旁路緩存模式

一般我們使用緩存,都是旁路緩存模式,它的特點就是讀的時候插入緩存,寫的時候刪除緩存

1)讀請求流程如下:

  • 讀的時候,先讀緩存,緩存命中的話,直接返回數據;
  • 緩存沒有命中的話,就去讀數據庫,從數據庫取出數據,放入緩存後,同時返迴響應。

2)寫流程如下:

這裡就有兩個問題思考:

1)為什麼寫請求要做刪除庫存操作,而不是做插入緩存動作?

2)為什麼是先操作數據庫在刪除舊的緩存,能對換一下順序嗎?

2、刪除緩存呢,還是更新緩存?

我們在操作緩存的時候,到底應該刪除緩存還是更新緩存呢?我們先來看個例子:

  1. 線程A先發起一個寫操作,第一步先更新數據庫;
  2. 線程B再發起一個寫操作,第二步更新了數據庫;
  3. 由於網絡等原因,線程B先更新了緩存;
  4. 線程A更新緩存。

這時候,緩存保存的是A的數據(老數據),數據庫保存的是B的數據(新數據),數據不一致了,臟數據出現啦。如果是刪除緩存取代更新緩存則不會出現這個臟數據問題。

3、先操作數據庫還是先操作緩存

雙寫的情況下,先操作數據庫還是先操作緩存?我們再來看一個例子:假設有A、B兩個請求,請求A做更新操作,請求B做查詢讀取操作。

  1. 線程A發起一個寫操作,第一步del cache;
  2. 此時線程B發起一個讀操作,cache miss;
  3. 線程B繼續讀DB,讀出來一個老數據;
  4. 然後線程B把老數據設置入cache;
  5. 線程A寫入DB最新的數據;

這樣就有問題啦,緩存和數據庫的數據不一致了。緩存保存的是老數據,數據庫保存的是新數據。因此,Cache-Aside緩存模式,選擇了先操作數據庫而不是先操作緩存。

聲明

公眾號如需轉載該篇文章,那麻煩在文章的頭部 聲明是轉至公眾號: 後端元宇宙。尊重作者辛苦勞動果實嘛。同時也可以問本人要該文章markdown原稿和原圖片。其它情況一律禁止轉載哦!