Redis快取設計與效能調優
阿新 • • 發佈:2022-03-29
快取設計
快取穿透
快取穿透是指查詢一個根本不存在的資料, 快取層和儲存層都不會命中, 通常出於容錯的考慮, 如果從儲存 層查不到資料則不寫入快取層。快取穿透將導致不存在的資料每次請求都要到儲存層去查詢, 失去了快取保護後端儲存的意義。 造成快取穿透的基本原因有兩個:
第一, 自身業務程式碼或者資料出現問題。
第二, 一些惡意攻擊、 爬蟲等造成大量空命中。
快取穿透問題解決方案:
1、快取空物件
1 String get(Stringkey){ 2 // 從快取中獲取資料 3 String cacheValue = cache.get(key); 4 // 快取為空 5 if (StringUtils.isBlank(cacheValue)) {6 // 從儲存中獲取 7 String storageValue = storage.get(key); 8 cache.set(key, storageValue); 9 // 如果儲存資料為空, 需要設定一個過期時間(300秒) 10 if (storageValue == null) { 11 cache.expire(key, 60 * 5); 12 } 13 return storageValue; 14 }else{ 15 // 快取非空 16 return cacheValue; 17 } 18 }
2、布隆過濾器
對於惡意攻擊,向伺服器請求大量不存在的資料造成的快取穿透,還可以用布隆過濾器先做一次過濾,對於不 存在的資料布隆過濾器一般都能夠過濾掉,不讓請求再往後端傳送。當布隆過濾器說某個值存在時,這個值可 能不存在;當它說不存在時,那就肯定不存在。布隆過濾器就是一個大型的位陣列和幾個不一樣的無偏 hash 函式。所謂無偏就是能夠把元素的 hash 值算得 比較均勻。
向布隆過濾器中新增 key 時,會使用多個 hash 函式對 key 進行 hash 算得一個整數索引值然後對位陣列長度 進行取模運算得到一個位置,每個 hash 函式都會算得一個不同的位置。再把位陣列的這幾個位置都置為 1 就 完成了 add 操作。
向布隆過濾器詢問 key 是否存在時,跟 add 一樣,也會把 hash 的幾個位置都算出來,看看位陣列中這幾個位 置是否都為 1,只要有一個位為 0,那麼說明布隆過濾器中這個key 不存在。如果都是 1,這並不能說明這個 key 就一定存在,只是極有可能存在,因為這些位被置為 1 可能是因為其它的 key 存在所致。如果這個位陣列 比較稀疏,這個概率就會很大,如果這個位陣列比較擁擠,這個概率就會降低。
這種方法適用於資料命中不高、 資料相對固定、 實時性低(通常是資料集較大) 的應用場景, 程式碼維護較為
複雜, 但是快取空間佔用很少。
可以用redisson實現布隆過濾器,引入依賴:
1 <dependency> 2 <groupId>org.redisson</groupId> 3 <artifactId>redisson</artifactId> 4 <version>3.6.5</version> 5 </dependency>示例虛擬碼:
1 package com.redisson; 2 import org.redisson.Redisson; 3 import org.redisson.api.RBloomFilter; 4 import org.redisson.api.RedissonClient; 5 import org.redisson.config.Config; 6 7 public class RedissonBloomFilter{ 8 public static void main(String[] args) { 9 Config config = new Config(); 10 config.useSingleServer().setAddress("redis://localhost:6379"); 11 //構造Redisson 12 RedissonClient redisson = Redisson.create(config); 13 14 RBloomFilter<String> bloomFilter = 15 redisson.getBloomFilter("nameList"); 16 //初始化布隆過濾器:預計元素為100000000L,誤差率為3%,根據這兩個引數 17 會計算出底層的bit陣列大小 18 bloomFilter.tryInit(100000000L,0.03); 19 //將zhuge插入到布隆過濾器中 20 bloomFilter.add("zhuge"); 21 //判斷下面號碼是否在布隆過濾器中 22 System.out.println(bloomFilter.contains("guojia"));//false 23 System.out.println(bloomFilter.contains("baiqi"));//false 24 System.out.println(bloomFilter.contains("zhuge"));//true 25 } 26 }使用布隆過濾器需要把所有資料提前放入布隆過濾器,並且在增加資料時也要往布隆過濾器裡放,布隆過濾器 快取過濾虛擬碼:
1 //初始化布隆過濾器 RBloomFilter<String>bloomFilter=redisson.getBloomFilter("nameList"); 2 //初始化布隆過濾器:預計元素為100000000L,誤差率為3% bloomFilter.tryInit(100000000L,0.03); 3 //把所有資料存入布隆過濾器 4 void init(){ 5 for (String key: keys) { 6 bloomFilter.put(key); 7 } 8 } 9 Stringget(Stringkey){ 10 // 從布隆過濾器這一級快取判斷下key是否存在 Boolean exist = 11 bloomFilter.contains(key); 12 if(!exist){ 13 return ""; 14 } 15 // 從快取中獲取資料 16 String cacheValue = cache.get(key); 17 // 快取為空 18 if (StringUtils.isBlank(cacheValue)) { 19 // 從儲存中獲取 20 String storageValue = storage.get(key); 21 cache.set(key, storageValue); 22 // 如果儲存資料為空, 需要設定一個過期時間(300秒) 23 if (storageValue == null) { 24 cache.expire(key, 60 * 5); } 25 return storageValue; 26 } else { 27 // 快取非空 28 return cacheValue; 29 } 30 }注意:布隆過濾器不能刪除資料,如果要刪除得重新初始化資料。
快取失效(擊穿)
由於大批量快取在同一時間失效可能導致大量請求同時穿透快取直達資料庫,可能會造成資料庫瞬間壓力過大 甚至掛掉,對於這種情況我們在批量增加快取時最好將這一批資料的快取過期時間設定為一個時間段內的不同 時間。示例虛擬碼:
1 Stringget(Stringkey){ 2 // 從快取中獲取資料 3 String cacheValue = cache.get(key); 4 // 快取為空 5 if (StringUtils.isBlank(cacheValue)) { // 從儲存中獲取 6 String storageValue = storage.get(key); cache.set(key, storageValue); //設定一個過期時間(300到600之間的一個隨機數) 7 int expireTime = new Random().nextInt(300) + 300; if (storageValue == null) { 8 cache.expire(key, expireTime); 9 } 10 return storageValue; } else { 11 // 快取非空 12 return cacheValue; 13 } }
快取雪崩
快取雪崩指的是快取層支撐不住或宕掉後, 流量會像奔逃的野牛一樣, 打向後端儲存層。 由於快取層承載著大量請求, 有效地保護了儲存層, 但是如果快取層由於某些原因不能提供服務(比如超大並 發過來,快取層支撐不住,或者由於快取設計不好,類似大量請求訪問bigkey,導致快取能支撐的併發急劇下 降), 於是大量請求都會打到儲存層, 儲存層的呼叫量會暴增, 造成儲存層也會級聯宕機的情況。 預防和解決快取雪崩問題, 可以從以下三個方面進行著手。1) 保證快取層服務高可用性,比如使用Redis Sentinel或Redis Cluster。
2) 依賴隔離元件為後端限流熔斷並降級。比如使用Sentinel或Hystrix限流降級元件。 比如服務降級,我們可以針對不同的資料採取不同的處理方式。當業務應用訪問的是非核心資料(例如電商商 品屬性,使用者資訊等)時,暫時停止從快取中查詢這些資料,而是直接返回預定義的預設降級資訊、空值或是 錯誤提示資訊;當業務應用訪問的是核心資料(例如電商商品庫存)時,仍然允許查詢快取,如果快取缺失, 也可以繼續通過資料庫讀取。
3) 提前演練。 在專案上線前, 演練快取層宕掉後, 應用以及後端的負載情況以及可能出現的問題, 在此基 礎上做一些預案設定。 熱點快取key重建優化
開發人員使用“快取+過期時間”的策略既可以加速資料讀寫, 又保證資料的定期更新, 這種模式基本能夠滿 足絕大部分需求。 但是有兩個問題如果同時出現, 可能就會對應用造成致命的危害:
- 當前key是一個熱點key(例如一個熱門的娛樂新聞),併發量非常大。
- 重建快取不能在短時間完成, 可能是一個複雜計算, 例如複雜的SQL、 多次IO、 多個依賴等。 在快取失效的瞬間, 有大量執行緒來重建快取, 造成後端負載加大, 甚至可能會讓應用崩潰。
要解決這個問題主要就是要避免大量執行緒同時重建快取。
我們可以利用互斥鎖來解決,此方法只允許一個執行緒重建快取, 其他執行緒等待重建快取的執行緒執行完, 重新從
快取獲取資料即可。
示例虛擬碼:
1 Stringget(Stringkey){ 2 // 從Redis中獲取資料 3 String value = redis.get(key); 4 // 如果value為空, 則開始重構快取 5 if (value == null) { 6 // 只允許一個執行緒重建快取, 使用nx, 並設定過期時間ex 7 String mutexKey = "mutext:key:" + key; 8 if (redis.set(mutexKey, "1", "ex 180", "nx")) { 9 // 從資料來源獲取資料 10 value = db.get(key); 11 // 回寫Redis, 並設定過期時間 12 redis.setex(key, timeout, value); 13 // 刪除key_mutex 14 redis.delete(mutexKey); 15 } 16 // 其他執行緒休息50毫秒後重試 17 else { 18 Thread.sleep(50); 19 get(key); 20 } 21 } 22 return value; 23 }快取與資料庫雙寫不一致
在大併發下,同時操作資料庫與快取會存在資料不一致性問題 1、雙寫不一致情況
2、讀寫併發不一致
解決方案:
1、對於併發機率很小的資料(如個人維度的訂單資料、使用者資料等),這種幾乎不用考慮這個問題,很少會發生 快取不一致,可以給快取資料加上過期時間,每隔一段時間觸發讀的主動更新即可。
2、就算併發很高,如果業務上能容忍短時間的快取資料不一致(如商品名稱,商品分類選單等),快取加上過期 時間依然可以解決大部分業務對於快取的要求。
3、如果不能容忍快取資料不一致,可以通過加分散式讀寫鎖保證併發讀寫或寫寫的時候按順序排好隊,讀讀的 時候相當於無鎖。
4、也可以用阿里開源的canal通過監聽資料庫的binlog日誌及時的去修改快取,但是引入了新的中介軟體,增加 了系統的複雜度。
總結:
以上我們針對的都是讀多寫少的情況加入快取提高效能,如果寫多讀多的情況又不能容忍快取資料不一致,那 就沒必要加快取了,可以直接操作資料庫。當然,如果資料庫抗不住壓力,還可以把快取作為資料讀寫的主存 儲,非同步將資料同步到資料庫,資料庫只是作為資料的備份。
放入快取的資料應該是對實時性、一致性要求不是很高的資料。切記不要為了用快取,同時又要保證絕對的一 致性做大量的過度設計和控制,增加系統複雜性!