Redis緩存的設計


一、Redis的緩存設計不合理會存在的問題

Redis作為緩存,但是緩存設計的不合理就會有以下的問題:

  • 緩存失效 
  • 緩存穿透
  • 緩存雪崩

緩存失效

由於大批量的緩存在同一個時間點失效,可能造成大量請求同時穿透緩存直達數據庫,可能造成數據庫的壓力瞬間增大,甚至數據庫掛掉的情況。

例如:熱點緩存在初始化的時候,會有拿出很多的數據,為保證數據的最新特性,一般都會設置一個超時時間;但是當這個超時時間到的時候,數據緩存就會全部失效,造成所有請求壓力全部作用到數據庫上。

解決方法在緩存初始化的時候,超時時間設置的不一樣。

偽代碼,如下:

String get(String key) {
	// 從緩存中獲取數據
	String cacheValue = cache.get(key);
	
	// 緩存為空
	if (StringUtils.isBlank(cacheValue)) {
		// 從存儲中獲取
		String storageValue = storage.get(key);
		cache.set(key, storageValue);
		
		//設置一個過期時間(300到600之間的一個隨機數)
		int expireTime = new Random().nextInt(300) + 300;
		if (storageValue == null) {
			cache.expire(key, expireTime);
		}
		 return storageValue;
	 } else {
		 // 緩存非空
		 return cacheValue;
	 }
 }

緩存穿透  

緩存穿透是指查詢一個根本不存在的數據,緩存層不會命中,大量的請求全部落到數據庫存儲層上,嚴重時造成數據庫掛掉。

通常是出於容錯的考慮,如果從存儲層查詢不到的不到數據,則不寫入到緩存層。

造成緩存穿透的原因主要有兩個:

(1)自身業務代碼或數據出現問題;

(2)一些惡意攻擊、爬蟲等造成大量空命中;

解決方法

方法一:將空對象緩存到Redis,並設置超時時間;但是若黑客制造了上千萬個key,那存儲到redis就會占用很大的空間。

偽代碼如下:

String get(String key) {
	// 從緩存中獲取數據
	String cacheValue = cache.get(key);
	// 緩存為空
	if (StringUtils.isBlank(cacheValue)) {
		// 從存儲中獲取
		String storageValue = storage.get(key);
		cache.set(key, storageValue);
		
		// 如果存儲數據為空, 需要設置一個過期時間(300秒)
		if (storageValue == null) {
			cache.expire(key, 60 * 5);
		}
		return storageValue;
	} else {
		// 緩存非空
		return cacheValue;
	}
}

 

方式二布隆過濾器

   對於惡意攻擊,向服務器請求大量不存在的數據造成的緩存穿透,還可以用布隆過濾器先做一次過濾,對於不存在的數據布隆過濾器一般都能夠過濾掉,不讓請求再往后端發送。當布隆過濾器說某個值存在時,這個值可能不存在;當它說不存在時,那就肯定不存在

  布隆過濾器的底層實際上一個大型的二進制數組(BitArray,即里面只能放0和1,它的里面存儲的不是真正的值而是0或1;真正的數據值經過hash函數計算后,得到一個數字n,那么就設置 BitArray[n] = 1 (即數據為n的下標存儲的值變為1)。為了防止 hash沖突,可以使用多個 hash函數經過多次計算得到。因為hash沖突的存在,所以說某個值存在時,它可能不存在;當它不存在時,那肯定就不存在。

  布隆過濾器占用的空間很少,效率也高。

(1)用guvua包自帶的布隆過濾器,引入依賴

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>22.0</version>
</dependency>

(2)示例偽代碼

import com.google.common.hash.BloomFilter;

//初始化布隆過濾器
//1000:期望存入的數據個數,0.001:期望的誤差率
BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("utf‐8")), 1000, 0.001);

//把所有數據存入布隆過濾器
void init(){
	for (String key: keys) {
	    bloomFilter.put(key);
	}
}

String get(String key) {
	// 從布隆過濾器這一級緩存判斷下key是否存在
	Boolean exist = bloomFilter.mightContain(key);
	if(!exist){
	        return "";
	}
	
	// 從緩存中獲取數據
	String cacheValue = cache.get(key);
	// 緩存為空
	if (StringUtils.isBlank(cacheValue)) {
		// 從存儲中獲取
		String storageValue = storage.get(key);
		cache.set(key, storageValue);
		
		// 如果存儲數據為空, 需要設置一個過期時間(300秒)
		if (storageValue == null) {
			cache.expire(key, 60 * 5);
		}
		return storageValue;
	} else {
		// 緩存非空
		return cacheValue;
	}
}

 注意此處使用的是單機版的布隆過濾器,實際上 Redisson 也實現了布隆過濾器。

緩存雪崩

緩存雪崩指的是緩存層支撐不住或掛掉后,流量全部作用到存儲層上,造成存儲層也給掛掉。

解決方法

(1)保證緩存層服務的高可用,使用 Redis的哨兵或Redis的集群方式;

(2)依賴隔離組件為后端限流和降級。使用 springCloud 的組件 Hystrix 來限流降級;

熱點緩存key重建

Redis的緩存層中沒有數據,但是在同一時刻獲取該數據的請求多達幾十W的QPS,造成這么多的請求全部作用到數據庫上。

例如:某一個突發的新聞,但是沒有緩存到Redis,但是同一個時刻查看該新聞的人多達幾十萬,造成這么多的請求全部作用到數據庫上,導致數據庫掛掉。

解決方法:並發量較大的時候,可以讓一個請求去數據庫查詢,其他請求等待。(使用分布式鎖實現

偽代碼如下:

String get(String key) {
	// 從Redis中獲取數據
	String value = redis.get(key);
	
	// 如果value為空, 則開始重構緩存
	if (value == null) {
	
		// 只允許一個線程重建緩存, 使用setnx, 並設置過期時間ex
		String mutexKey = "mutext:key:" + key;
		if (redis.set(mutexKey, "1", "ex 180", "nx")) {
			// 從數據源獲取數據
			value = db.get(key);
// 回寫Redis, 並設置過期時間 redis.setex(key, timeout, value);13 // 刪除key_mutex redis.delete(mutexKey); } // 其他線程休息50毫秒后重試 else {         Thread.sleep(50);
get(key); } } return value; }

  

二、Redis使用的規范

1、鍵值的設計

(1)key的命名

  • 建議】以業務名或數據庫名為前綴,用冒號分割;  trade:order:1 
  • 建議】保證語義的前提下,控制 key 的長度(key比較多時占用的內存比較大)
user:{uid}:friends:messages:{mid} 
簡化為
u:{uid}:fr:m:{mid}

(2)value 的設計

  • 強制】拒絕 bigkey (防止網卡流量、查詢慢)

在Redis中,一個字符串最大512MB,一個二級數據結構(例如hash、list、set、zset)可以存儲大約40億個(2^32-1)個元素,但實際中如果下面兩種情況,我就會認為它是bigkey。

  a. 字符串類型:它的big體現在單個value值很大,一般認為超過10KB就是bigkey。

  b. 非字符串類型:哈希(hash)、列表(list)、集合(set)、有序集合(zset),它們的big體現在元素個數太多,不要超過5000個。

問題:假如出現了非字符串類型的 bigkey,我們怎么去處理呢?

解答:非字符串的bigkey,不要使用del刪除,使用hscan、sscan、zscan方式漸進式刪除,同時要注意防止bigkey過期時間自動刪除問題(例如一個200萬的zset設置1小時過期,會觸發del操作,造成阻塞)

BigKey的危害

  • 導致Redis的阻塞;
  • 造成網絡阻塞;

  bigkey也就意味着每次獲取要產生的網絡流量較大,假設一個bigkey為1MB,客戶端每秒訪問量為1000,那么每秒產生1000MB的流量,對於普通的千兆網卡(按照字節算是128MB/s)的服務器來說簡直是滅頂之災,而且一般服務器會采用單機多實例的方式來部署,也就是說一個bigkey可能會對其他實例也造成影響,其后果不堪設想。

  有個bigkey,它安分守己(只執行簡單的命令,例如hget、lpop、zscore等),但它設置了過期時間,當它過期后,會被刪除,如果沒有使用Redis 4.0的過期異步刪除(lazyfree-lazyexpire yes),就會存在阻塞Redis的可能性。

  • 建議】選擇適合的數據類型。

例如:實體類型(要合理控制和使用數據結構,但也要注意節省內存和性能之間的平衡)

反例:
set user:1:name tom
set user:1:age 19
set user:1:favor football

正例:
hmset user:1 name tom age 19 favor football

2、命令的使用

  • 【推薦】遍歷的需求可以使用hscan、sscan、zscan,來代替 hgetall、lrange、smembers、zrange、sinter等。
  • 【推薦】禁用命令;通過redis的rename機制禁掉命令:keys、flushall、flushdb等。
  • 【推薦】使用批量操作提高效率;但要注意控制一次批量操作的元素個數(例如500以內,實際也和元素字節數有關)。
原生命令:例如mget、mset。
非原生命令:可以使用pipeline提高效率。

注意兩者不同:
 a. 原生是原子操作,pipeline是非原子操作;
 b. pipeline可以打包不同的命令,原生做不到;
 c. pipeline需要客戶端和服務端同時支持。

  • 【建議】Redis事務功能較弱,不建議過多使用,可以用lua替代。

3、客戶端使用

  • 【推薦】避免多個應用使用一個Redis實例。正例:不相干的業務拆分,公共數據做服務化。
  • 【推薦】使用帶有連接池的數據庫,可以有效控制連接,同時提高效率。

  連接池參數說明

參數名 含義 默認值 使用建議
maxTotal 資源池中最大連接數 8  
maxIdle 資源池允許最大空閑
的連接數
8  
minIdle 資源池確保最少空閑
的連接數
0  
blockWhenExhausted
當資源池用盡后,調用者是否要等待。只有當為true時,下面的maxWaitMillis才會生效
true 建議使用默認值
maxWaitMillis 當資源池連接用盡后,調用者的最大等待時間(單位為毫秒)
-1:表示永不超時 不建議使用默認值
testOnBorrow 向資源池借用連接時是否做連接有效性檢測(ping),無效連接會被移除
false 業務量很大時候建議
設置為false(多一次
ping的開銷)。
testOnReturn 向資源池歸還連接時是否做連接有效性檢測(ping),無效連接會被移除
false 業務量很大時候建議
設置為false(多一次
ping的開銷)。
jmxEnabled 是否開啟jmx監控,可用於監控

true 建議開啟,但應用本身也有開啟


(1)maxTotal
:最大連接數,早期的版本叫 maxActive; 

設置 maxTotal 的值時,需要考慮以下場景:

  • 業務希望Redis並發量;
  • 客戶端執行命令時間;
  • Redis資源:例如 nodes(例如應用個數) * maxTotal 是不能超過redis的配置文件中最大連接數 maxclients。
  • 資源開銷:例如雖然希望控制空閑連接(連接池此刻可馬上使用的連接),但是不希望因為連接池的頻繁釋放創建連接造成不必靠開銷,例如:

  假設一次Redis命令時間的平均耗時約為1ms,那么一個連接的QPS大約是1000;業務期望的QPS是50000,那么理論上需要的資源池大小是50000 / 1000 = 50個。但事實上這是個理論值,還要考慮到要比理論值預留一些資源,通常來講maxTotal可以比理論值大一些。但這個值不是越大越好,一方面連接太多占用客戶端和服務端資源,另一方面對於Redis這種高QPS的服務器,一個大命令的阻塞即使設置再大資源池仍然會無濟於事。
(2)maxIdle 

  maxIdle實際上才是業務需要的最大連接數,maxTotal是為了給出余量,所以maxIdle不要設置過小,否則會有new Jedis(新連接)開銷。
連接池的最佳性能是maxTotal = maxIdle(maxTotal設置過大就不能一樣),這樣就避免連接池伸縮帶來的性能干擾。但是如果並發量不大或者maxTotal設置過高,會導致不必要的連接資源浪費。一般推薦maxIdle可以設置為按上面的業務期望QPS計算出來的理論連接數,maxTotal可以再放大一倍。

(3)minIdle

  minIdle(最小空閑連接數),與其說是最小空閑連接數,不如說是"至少需要保持的空閑連接數",在使用連接的過程中,如果連接數超過了minIdle,那么繼續建立連接,如果超過了maxIdle,當超過的連接執行完業務后會慢慢被移出連接池釋放掉。

 Redis連接池創建的過程

假如Redis的連接池設置參數:maxTotal=100,maxIdle=50,minIdle=10;

(1)業務服務啟動之后連接池是不會去創建連接的,接着業務系統要去連接池里面拿連接操作Redis,這才會 new Jedis操作,用完之后就會把該連接放入到連接池里面;

(2)當並發量比較大變為 70 的時候,這個時候連接池里面創建了 70個連接;

(3)過來一會,Redis的並發量變得比較小了,就會慢慢的去釋放連接池中多余的 70 - 50 = 20個連接;默認釋放到 maxIdle=50 就可以了;

  如果系統啟動完馬上就會有很多的請求過來,那么我們可以給redis連接池做預熱,比如快速的創建一些redis連接,執行簡單命令,類似ping(),快速的將連接池里的空閑連接提升到minIdle的數量。要根據實際系統的QPS和調用redis客戶端的規模整體評估每個節點所使用的連接池大小。

連接池預熱實例代碼:

List<Jedis> minIdleJedisList = new ArrayList<Jedis>(jedisPoolConfig.getMinIdle());

for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
	Jedis jedis = null;
	try {
		jedis = pool.getResource();
		minIdleJedisList.add(jedis);
		jedis.ping();
	} 
	catch (Exception e) {
		logger.error(e.getMessage(), e);
	} 
	finally {
	    //注意,這里不能馬上close將連接還回連接池,否則最后連接池里只會建立1個連接。。
	    //jedis.close();
	}
 }
 
 //統一將預熱的連接還回連接池
 for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {
	 Jedis jedis = null;
	 try {
		 jedis = minIdleJedisList.get(i);
		 //將連接歸還回連接池
		 jedis.close();
	 } 
	 catch (Exception e) {
		logger.error(e.getMessage(), e);
	 } 
	 finally {
	 }
 }

Redis的過期鍵的三種清除策略

  • 被動刪除:當讀/寫一個已經過期的key時,會觸發惰性刪除策略,直接刪除掉這個過期key;
  • 主動刪除:由於惰性刪除策略無法保證冷數據被及時刪掉,所以Redis會定期主動淘汰一批已過期的key;
  • 當前已經使用內存超過 maxmemory 限定時,會觸發主動清理策略

  Redis一定要根據實際情況設置 maxmemory, 因為若不設置 maxmemory 就會一直使用物理內存,物理內存使用完之后就會去使用磁盤,那么Redis的性能就會急劇的下降。  

  當REDIS運行在主從模式時,只有主結點才會執行被動和主動這兩種過期刪除策略,然后把刪除操作”del key”同步到從結點。

當前已用內存超過maxmemory限定時,會觸發主動清理策略

默認策略是volatile-lru,即超過最大內存后,在過期鍵中使用lru算法進行key的剔除,保證不過期數據不被刪除,但是可能會出現OOM問題。

其他策略如下:

  • allkeys-lru:根據LRU算法刪除鍵,不管數據有沒有設置超時屬性,直到騰出足夠空間為止。
  • allkeys-random:隨機刪除所有鍵,直到騰出足夠空間為止。
  • volatile-random: 隨機刪除過期鍵,直到騰出足夠空間為止。
  • volatile-ttl:根據鍵值對象的ttl屬性,刪除最近將要過期數據。如果沒有,回退到noeviction策略。
  • noeviction:不會剔除任何數據,拒絕所有寫入操作並返回客戶端錯誤信息"(error)OOM command not allowed when used memory",此時Redis只響應讀操作。



免責聲明!

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



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