首頁  >  文章  >  資料庫  >  21個使用Redis時必須注意的重點(總結)

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

青灯夜游
青灯夜游轉載
2021-03-24 11:31:052810瀏覽

這篇文章跟大家分享使用Redis必須知道的21個注意要點。有一定的參考價值,有需要的朋友可以參考一下,希望對大家有幫助。

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

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個使用Redis時必須注意的重點(總結)

#反例:

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

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

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

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

2、Redis 有坑的那些指令

##2.1. 慎用O(n)<span style="font-size: 18px;"></span>複雜度指令,如#hgetall<span style="font-size: 18px;"></span>##、 smember<span style="font-size: 18px;"></span>,lrange<span style="font-size: 18px;"></span>等##因為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内存。

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

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) <p>这种方案的<strong>缺点</strong>:</p><blockquote><ul>
<li>过期时间是客户端自己生成的,分布式环境下,每个客户端的时间必须同步</li>
<li>没有保存持有者的唯一标识,可能被别的客户端释放/解锁。</li>
<li>锁过期的时候,并发多个客户端同时请求过来,都执行了<code>jedis.getSet()</code>,最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖。</li>
</ul></blockquote><p><strong>3.1.3: SET的扩展命令(SET EX PX NX)(注意可能存在的问题)</strong></p><pre class="brush:php;toolbar:false">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()释放锁的时候,可能这把锁已经不属于当前客户端,会解除他人加的锁。

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

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

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

以上的分布式锁,还存在单机问题: 

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

如果线程一在Redis的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以获取同个key的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。

针对单机问题,可以使用Redlock算法。有兴趣的朋友可以看下我这篇文章哈,七种方案!探讨Redis分布式锁的正确使用姿势

3.2 缓存一致性注意点

  • 如果是读请求,先读缓存,后读数据库
  • 如果写请求,先更新数据库,再写缓存
  • 每次更新数据后,需要清除缓存
  • 缓存一般都需要设置一定的过期失效
  • 一致性要求高的话,可以使用biglog+MQ保证。

有兴趣的朋友,可以看下我这篇文章哈:并发环境下,先操作数据库还是先操作缓存?

3.3 合理评估Redis容量,避免由于频繁set覆盖,导致之前设置的过期时间无效。

我们知道,Redis的所有数据结构类型,都是可以设置过期时间的。假设一个字符串,已经设置了过期时间,你再去重新设置它,就会导致之前的过期时间无效。

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

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 缓存穿透问题

先来看一个常见的缓存使用方式:读请求来了,先查下缓存,缓存有值命中,就直接返回;缓存没命中,就去查数据库,然后把数据库的值更新到缓存,再返回。

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

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

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

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

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

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

更多程式相關知識,請造訪:程式設計影片! !

以上是21個使用Redis時必須注意的重點(總結)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除