21個使用Redis時必須注意的要點(總結)

2021-03-24 13:00:43
本篇文章給大家分享使用Redis必須知道的21個注意要點。有一定的參考價值,有需要的朋友可以參考一下,希望對大家有所幫助。

1、Redis的使用規範

1.1、 key的規範要點

我們設計Redis的key的時候,要注意以下這幾個點:

  • 以業務名為key字首,用冒號隔開,以防止key衝突覆蓋。如,live:rank:1
  • 確保key的語意清晰的情況下,key的長度儘量小於30個字元。
  • key禁止包含特殊字元,如空格、換行、單雙引號以及其他跳脫字元。
  • Redis的key儘量設定ttl,以保證不使用的Key能被及時清理或淘汰。

1.2、value的規範要點

Redis的value值不可以隨意設定的哦。

第一點,如果大量儲存bigKey是會有問題的,會導致慢查詢,記憶體增長過快等等。

  • 如果是String型別,單個value大小控制10k以內。
  • 如果是hash、list、set、zset型別,元素個數一般不超過5000。

第二點,要選擇適合的資料型別。不少小夥伴只用Redis的String型別,上來就是set和get。實際上,Redis 提供了豐富的資料結構型別,有些業務場景,更適合hash、zset等其他資料結果。【相關推薦:Redis視訊教學

1.png

反例:

set user:666:name jay
set user:666:age 18

正例

hmset user:666 name jay age 18

1.3. 給Key設定過期時間,同時注意不同業務的key,儘量過期時間分散一點

  • 因為Redis的資料是存在記憶體中的,而記憶體資源是很寶貴的。
  • 我們一般是把Redis當做快取來用,而不是資料庫,所以key的生命週期就不宜太長久啦。
  • 因此,你的key,一般建議用expire設定過期時間

如果大量的key在某個時間點集中過期,到過期的那個時間點,Redis可能會存在卡頓,甚至出現快取雪崩現象,因此一般不同業務的key,過期時間應該分散一些。有時候,同業務的,也可以在時間上加一個隨機值,讓過期時間分散一些。

1.4.建議使用批次操作提高效率

我們日常寫SQL的時候,都知道,批次操作效率會更高,一次更新50條,比迴圈50次,每次更新一條效率更高。其實Redis操作命令也是這個道理。

Redis使用者端執行一次命令可分為4個過程:1.傳送命令-> 2.命令排隊-> 3.命令執行-> 4. 返回結果。1和4 稱為RRT(命令執行往返時間)。 Redis提供了批次操作命令,如mget、mset等,可有效節約RRT。但是呢,大部分的命令,是不支援批次操作的,比如hgetall,並沒有mhgetall存在。Pipeline 則可以解決這個問題。

Pipeline是什麼呢?它能將一組Redis命令進行組裝,通過一次RTT傳輸給Redis,再將這組Redis命令的執行結果按順序返回給使用者端.

我們先來看下沒有使用Pipeline執行了n條命令的模型:

2.png

使用Pipeline執行了n次命令,整個過程需要1次RTT,模型如下:

3.png

2、Redis 有坑的那些命令

2.1. 慎用O(n)複雜度命令,如hgetallsmemberlrange

因為Redis是單執行緒執行命令的。hgetall、smember等命令時間複雜度為O(n),當n持續增加時,會導致 Redis CPU 持續飆高,阻塞其他命令的執行。

hgetall、smember,lrange等這些命令不是一定不能使用,需要綜合評估資料量,明確n的值,再去決定。 比如hgetall,如果雜湊元素n比較多的話,可以優先考慮使用hscan

2.2 慎用Redis的monitor命令

Redis Monitor 命令用於實時列印出Redis伺服器接收到的命令,如果我們想知道使用者端對redis伺服器端做了哪些命令操作,就可以用Monitor 命令檢視,但是它一般偵錯用而已,儘量不要在生產上用!因為monitor命令可能導致redis的記憶體持續飆升。

monitor的模型是醬紫的,它會將所有在Redis伺服器執行的命令進行輸出,一般來講Redis伺服器的QPS是很高的,也就是如果執行了monitor命令,Redis伺服器在Monitor這個使用者端的輸出緩衝區又會有大量「存貨」,也就佔用了大量Redis記憶體。

4.png

2.3、生產環境不能使用 keys指令

Redis Keys 命令用於查詢所有符合給定模式pattern的key。如果想檢視Redis 某型別的key有多少個,不少小夥伴想到用keys命令,如下:

keys key字首*

但是,redis的keys是遍歷匹配的,複雜度是O(n),資料庫資料越多就越慢。我們知道,redis是單執行緒的,如果資料比較多的話,keys指令就會導致redis執行緒阻塞,線上服務也會停頓了,直到指令執行完,服務才會恢復。因此,一般在生產環境,不要使用keys指令。官方檔案也有宣告:

Warning: consider KEYS as a command that should only be used in production environments with extreme care. It may ruin performance when it is executed against large databases. This command is intended for debugging and special operations, such as changing your keyspace layout. Don't use KEYS in your regular application code. If you're looking for a way to find keys in a subset of your keyspace, consider using sets.

其實,可以使用scan指令,它同keys命令一樣提供模式匹配功能。它的複雜度也是 O(n),但是它通過遊標分步進行,不會阻塞redis執行緒;但是會有一定的重複概率,需要在使用者端做一次去重

scan支援增量式迭代命令,增量式迭代命令也是有缺點的:舉個例子, 使用 SMEMBERS 命令可以返回集合鍵當前包含的所有元素, 但是對於 SCAN 這類增量式迭代命令來說, 因為在對鍵進行增量式迭代的過程中, 鍵可能會被修改, 所以增量式迭代命令只能對被返回的元素提供有限的保證 。

2.4 禁止使用flushall、flushdb

  • Flushall 命令用於清空整個 Redis 伺服器的資料(刪除所有資料庫的所有 key )。
  • Flushdb 命令用於清空當前資料庫中的所有 key。

這兩命令是原子性的,不會終止執行。一旦開始執行,不會執行失敗的。

2.5 注意使用del命令

刪除key你一般使用什麼命令?是直接del?如果刪除一個key,直接使用del命令當然沒問題。但是,你想過del的時間複雜度是多少嘛?我們分情況探討一下:

  • 如果刪除一個String型別的key,時間複雜度就是O(1)可以直接del
  • 如果刪除一個List/Hash/Set/ZSet型別時,它的複雜度是O(n), n表示元素個數。

因此,如果你刪除一個List/Hash/Set/ZSet型別的key時,元素越多,就越慢。當n很大時,要尤其注意,會阻塞主執行緒的。那麼,如果不用del,我們應該怎麼刪除呢?

  • 如果是List型別,你可以執行lpop或者rpop,直到所有元素刪除完成。
  • 如果是Hash/Set/ZSet型別,你可以先執行hscan/sscan/scan查詢,再執行hdel/srem/zrem依次刪除每個元素。

2.6 避免使用SORT、SINTER等複雜度過高的命令。

執行復雜度較高的命令,會消耗更多的 CPU 資源,會阻塞主執行緒。所以你要避免執行如SORT、SINTER、SINTERSTORE、ZUNIONSTORE、ZINTERSTORE等聚合命令,一般建議把它放到使用者端來執行。

3、專案實戰避坑操作

3.1 分散式鎖使用的注意點

分散式鎖其實就是,控制分散式系統不同程序共同存取共用資源的一種鎖的實現。秒殺下單、搶紅包等等業務場景,都需要用到分散式鎖。我們經常使用Redis作為分散式鎖,主要有這些注意點:

3.1.1 兩個命令SETNX + EXPIRE分開寫(典型錯誤實現範例)

if(jedis.setnx(key_resource_id,lock_value) == 1){ //加鎖
    expire(key_resource_id,100); //設定過期時間
    try {
        do something  //業務請求
    }catch(){
  }
  finally {
       jedis.del(key_resource_id); //釋放鎖
    }
}

如果執行完setnx加鎖,正要執行expire設定過期時間時,程序crash或者要重新啟動維護了,那麼這個鎖就「長生不老」了,別的執行緒永遠獲取不到鎖啦,所以一般分散式鎖不能這麼實現。

3.1.2 SETNX + value值是過期時間 (有些小夥伴是這麼實現,有坑)

long expires = System.currentTimeMillis() + expireTime; //系統時間+設定的過期時間
String expiresStr = String.valueOf(expires);

// 如果當前鎖不存在,返回加鎖成功
if (jedis.setnx(key_resource_id, expiresStr) == 1) {
        return true;
} 
// 如果鎖已經存在,獲取鎖的過期時間
String currentValueStr = jedis.get(key_resource_id);

// 如果獲取到的過期時間,小於系統當前時間,表示已經過期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {

     // 鎖已過期,獲取上一個鎖的過期時間,並設定現在鎖的過期時間(不瞭解redis的getSet命令的小夥伴,可以去官網看下哈)
    String oldValueStr = jedis.getSet(key_resource_id, expiresStr);
    
    if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
         // 考慮多執行緒並行的情況,只有一個執行緒的設定值和當前值相同,它才可以加鎖
         return true;
    }
}
        
//其他情況,均返回加鎖失敗
return false;
}

這種方案的缺點

  • 過期時間是使用者端自己生成的,分散式環境下,每個使用者端的時間必須同步
  • 沒有儲存持有者的唯一標識,可能被別的使用者端釋放/解鎖。
  • 鎖過期的時候,並行多個使用者端同時請求過來,都執行了jedis.getSet(),最終只能有一個使用者端加鎖成功,但是該使用者端鎖的過期時間,可能被別的使用者端覆蓋。

3.1.3: SET的擴充套件命令(SET EX PX NX)(注意可能存在的問題)

if(jedis.set(key_resource_id, lock_value, "NX", "EX", 100s) == 1){ //加鎖
    try {
        do something  //業務處理
    }catch(){
  }
  finally {
       jedis.del(key_resource_id); //釋放鎖
    }
}

這個方案還是可能存在問題:

  • 鎖過期釋放了,業務還沒執行完。
  • 鎖被別的執行緒誤刪。

3.1.4 SET EX PX NX + 校驗唯一隨機值,再刪除(解決了誤刪問題,還是存在鎖過期,業務沒執行完的問題)

if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1){ //加鎖
    try {
        do something  //業務處理
    }catch(){
  }
  finally {
       //判斷是不是當前執行緒加的鎖,是才釋放
       if (uni_request_id.equals(jedis.get(key_resource_id))) {
        jedis.del(lockKey); //釋放鎖
        }
    }
}

在這裡,判斷是不是當前執行緒加的鎖和釋放鎖不是一個原子操作。如果呼叫jedis.del()釋放鎖的時候,可能這把鎖已經不屬於當前使用者端,會解除他人加的鎖。

5.png

一般也是用lua指令碼代替。lua指令碼如下:

if redis.call('get',KEYS[1]) == ARGV[1] then 
   return redis.call('del',KEYS[1]) 
else
   return 0
end;

3.1.5 Redisson框架 + Redlock演演算法 解決鎖過期釋放,業務沒執行完問題+單機問題

Redisson 使用了一個Watch dog解決了鎖過期釋放,業務沒執行完問題,Redisson原理圖如下:

6.png

以上的分散式鎖,還存在單機問題:

7.png

如果執行緒一在Redis的master節點上拿到了鎖,但是加鎖的key還沒同步到slave節點。恰好這時,master節點發生故障,一個slave節點就會升級為master節點。執行緒二就可以獲取同個key的鎖啦,但執行緒一也已經拿到鎖了,鎖的安全性就沒了。

針對單機問題,可以使用Redlock演演算法。有興趣的朋友可以看下我這篇文章哈,七種方案!探討Redis分散式鎖的正確使用姿勢

3.2 快取一致性注意點

  • 如果是讀請求,先讀快取,後讀資料庫
  • 如果寫請求,先更新資料庫,再寫快取
  • 每次更新資料後,需要清除快取
  • 快取一般都需要設定一定的過期失效
  • 一致性要求高的話,可以使用biglog+MQ保證。

有興趣的朋友,可以看下我這篇文章哈:並行環境下,先運算元據庫還是先操作快取?

3.3 合理評估Redis容量,避免由於頻繁set覆蓋,導致之前設定的過期時間無效。

我們知道,Redis的所有資料結構型別,都是可以設定過期時間的。假設一個字串,已經設定了過期時間,你再去重新設定它,就會導致之前的過期時間無效。

8.png

Redis setKey原始碼如下:

void setKey(redisDb *db,robj *key,robj *val) {
    if(lookupKeyWrite(db,key)==NULL) {
       dbAdd(db,key,val);
    }else{
    dbOverwrite(db,key,val);
    }
    incrRefCount(val);
    removeExpire(db,key); //去掉過期時間
    signalModifiedKey(db,key);
}

實際業務開發中,同時我們要合理評估Redis的容量,避免頻繁set覆蓋,導致設定了過期時間的key失效。新手小白容易犯這個錯誤。

3.4 快取穿透問題

先來看一個常見的快取使用方式:讀請求來了,先查下快取,快取有值命中,就直接返回;快取沒命中,就去查資料庫,然後把資料庫的值更新到快取,再返回。

9.png

快取穿透:指查詢一個一定不存在的資料,由於快取是不命中時需要從資料庫查詢,查不到資料則不寫入快取,這將導致這個不存在的資料每次請求都要到資料庫去查詢,進而給資料庫帶來壓力。

通俗點說,讀請求存取時,快取和資料庫都沒有某個值,這樣就會導致每次對這個值的查詢請求都會穿透到資料庫,這就是快取穿透。

快取穿透一般都是這幾種情況產生的:

  • 業務不合理的設計,比如大多數使用者都沒開守護,但是你的每個請求都去快取,查詢某個userid查詢有沒有守護。
  • 業務/運維/開發失誤的操作,比如快取和資料庫的資料都被誤刪除了。
  • 駭客非法請求攻擊,比如駭客故意捏造大量非法請求,以讀取不存在的業務資料。

如何避免快取穿透呢? 一般有三種方法。

  • 如果是非法請求,我們在API入口,對引數進行校驗,過濾非法值。
  • 如果查詢資料庫為空,我們可以給快取設定個空值,或者預設值。但是如有有寫請求進來的話,需要更新快取哈,以保證快取一致性,同時,最後給快取設定適當的過期時間。(業務上比較常用,簡單有效)
  • 使用布隆過濾器快速判斷資料是否存在。即一個查詢請求過來時,先通過布隆過濾器判斷值是否存在,存在才繼續往下查。
布隆過濾器原理:它由初始值為0的點陣圖陣列和N個雜湊函陣列成。一個對一個key進行N個hash演演算法獲取N個值,在位元陣列中將這N個值雜湊後設定為1,然後查的時候如果特定的這幾個位置都為1,那麼布隆過濾器判斷該key存在。

3.5 快取雪奔問題

快取雪奔: 指快取中資料大批次到過期時間,而查詢資料量巨大,請求都直接存取資料庫,引起資料庫壓力過大甚至down機。

  • 快取雪奔一般是由於大量資料同時過期造成的,對於這個原因,可通過均勻設定過期時間解決,即讓過期時間相對離散一點。如採用一個較大固定值+一個較小的隨機值,5小時+0到1800秒醬紫。
  • Redis 故障宕機也可能引起快取雪奔。這就需要構造Redis高可用叢集啦。

3.6 快取擊穿問題

快取擊穿: 指熱點key在某個時間點過期的時候,而恰好在這個時間點對這個Key有大量的並行請求過來,從而大量的請求打到db。

快取擊穿看著有點像,其實它兩區別是,快取雪奔是指資料庫壓力過大甚至down機,快取擊穿只是大量並行請求到了DB資料庫層面。可以認為擊穿是快取雪奔的一個子集吧。有些文章認為它倆區別,是區別在於擊穿針對某一熱點key快取,雪奔則是很多key。

解決方案就有兩種:

  • 1.使用互斥鎖方案。快取失效時,不是立即去載入db資料,而是先使用某些帶成功返回的原子操作命令,如(Redis的setnx)去操作,成功的時候,再去載入db資料庫資料和設定快取。否則就去重試獲取快取。
  • 2. 「永不過期」,是指沒有設定過期時間,但是熱點資料快要過期時,非同步執行緒去更新和設定過期時間。

3.7、快取熱key問題

在Redis中,我們把存取頻率高的key,稱為熱點key。如果某一熱點key的請求到伺服器主機時,由於請求量特別大,可能會導致主機資源不足,甚至宕機,從而影響正常的服務。

而熱點Key是怎麼產生的呢?主要原因有兩個:

  • 使用者消費的資料遠大於生產的資料,如秒殺、熱點新聞等讀多寫少的場景。
  • 請求分片集中,超過單Redi伺服器的效能,比如固定名稱key,Hash落入同一臺伺服器,瞬間存取量極大,超過機器瓶頸,產生熱點Key問題。

那麼在日常開發中,如何識別到熱點key呢?

  • 憑經驗判斷哪些是熱Key;
  • 使用者端統計上報;
  • 服務代理層上報

如何解決熱key問題?

  • Redis叢集擴容:增加分片副本,均衡讀流量;
  • 對熱key進行hash雜湊,比如將一個key備份為key1,key2……keyN,同樣的資料N個備份,N個備份分佈到不同分片,存取時可隨機存取N個備份中的一個,進一步分擔讀流量;
  • 使用二級快取,即JVM本地快取,減少Redis的讀請求。

4、Redis設定運維

4.1 使用長連線而不是短連線,並且合理設定使用者端的連線池

  • 如果使用短連線,每次都需要過 TCP 三次握手、四次揮手,會增加耗時。然而長連線的話,它建立一次連線,redis的命令就能一直使用,醬紫可以減少建立redis連線時間。
  • 連線池可以實現在使用者端建立多個連線並且不釋放,需要使用連線的時候,不用每次都建立連線,節省了耗時。但是需要合理設定引數,長時間不操作 Redis時,也需及時釋放連線資源。

4.2 只使用 db0

Redis-standalone架構禁止使用非db0.原因有兩個

  • 一個連線,Redis執行命令select 0和select 1切換,會損耗新能。
  • Redis Cluster 只支援 db0,要遷移的話,成本高

4.3 設定maxmemory + 恰當的淘汰策略。

為了防止記憶體積壓膨脹。比如有些時候,業務量大起來了,redis的key被大量使用,記憶體直接不夠了,運維小哥哥也忘記加大記憶體了。難道redis直接這樣掛掉?所以需要根據實際業務,選好maxmemory-policy(最大記憶體淘汰策略),設定好過期時間。一共有8種記憶體淘汰策略:

  • volatile-lru:當記憶體不足以容納新寫入資料時,從設定了過期時間的key中使用LRU(最近最少使用)演演算法進行淘汰;
  • allkeys-lru:當記憶體不足以容納新寫入資料時,從所有key中使用LRU(最近最少使用)演演算法進行淘汰。
  • volatile-lfu:4.0版本新增,當記憶體不足以容納新寫入資料時,在過期的key中,使用LFU演演算法進行刪除key。
  • allkeys-lfu:4.0版本新增,當記憶體不足以容納新寫入資料時,從所有key中使用LFU演演算法進行淘汰;
  • volatile-random:當記憶體不足以容納新寫入資料時,從設定了過期時間的key中,隨機淘汰資料;。
  • allkeys-random:當記憶體不足以容納新寫入資料時,從所有key中隨機淘汰資料。
  • volatile-ttl:當記憶體不足以容納新寫入資料時,在設定了過期時間的key中,根據過期時間進行淘汰,越早過期的優先被淘汰;
  • noeviction:預設策略,當記憶體不足以容納新寫入資料時,新寫入操作會報錯。

4.4 開啟 lazy-free 機制

Redis4.0+版本支援lazy-free機制,如果你的Redis還是有bigKey這種玩意存在,建議把lazy-free開啟。當開啟它後,Redis 如果刪除一個 bigkey 時,釋放記憶體的耗時操作,會放到後臺執行緒去執行,減少對主執行緒的阻塞影響。

10.png

更多程式設計相關知識,請存取:!!

以上就是21個使用Redis時必須注意的要點(總結)的詳細內容,更多請關注TW511.COM其它相關文章!