天天看點

使用Redis,必須知道的21個注意要點

作者:散文随風想

前言

參考阿裡的redis開發規範,以及Redis開發與運維這本書。分使用規範、有坑的指令、項目實戰操作、運維配置四個方向,整理了使用Redis的21個注意點,希望對大家有幫助,一起學習哈

使用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,必須知道的21個注意要點

「反例:」

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條指令的模型:

使用Redis,必須知道的21個注意要點

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

使用Redis,必須知道的21個注意要點

2、Redis 有坑的那些指令

2.1. 慎用O(n)複雜度指令,如hgetall、smember,lrange等

因為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記憶體。

使用Redis,必須知道的21個注意要點

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()釋放鎖的時候,可能這把鎖已經不屬于目前用戶端,會解除他人加的鎖。

使用Redis,必須知道的21個注意要點

一般也是用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原理圖如下:

使用Redis,必須知道的21個注意要點

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

使用Redis,必須知道的21個注意要點

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

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

3.2 緩存一緻性注意點

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

有興趣的朋友,可以看下我這篇文章哈:并發環境下,先操作資料庫還是先操作緩存?

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

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

使用Redis,必須知道的21個注意要點

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 緩存穿透問題

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

使用Redis,必須知道的21個注意要點

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

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

緩存穿透一般都是這幾種情況産生的:

  • 「業務不合理的設計」,比如大多數使用者都沒開守護,但是你的每個請求都去緩存,查詢某個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 時,釋放記憶體的耗時操作,會放到背景線程去執行,減少對主線程的阻塞影響。

使用Redis,必須知道的21個注意要點

參考與感謝

  • Redis 千萬不要亂用KEYS指令,不然會挨打的[1]
  • 阿裡雲Redis開發規範[2]
  • Redis 最佳實踐指南:7個次元+43條使用規範
  • Redis的緩存穿透及解決方法——布隆過濾器BloomFilter[3]
  • Redis 緩存性能實踐及總結[4]

Reference

[1]

Redis 千萬不要亂用KEYS指令,不然會挨打的:https://www.cnblogs.com/tonyY/p/12175032.html

[2]

阿裡雲Redis開發規範:https://developer.aliyun.com/article/531067

[3]

Redis的緩存穿透及解決方法——布隆過濾器BloomFilter:https://blog.csdn.net/wx1528159409/article/details/88357728

[4]

Redis 緩存性能實踐及總結:https://www.shangmayuan.com/a/d2f178b548a64c25854a9750.html

繼續閱讀