這篇文章跟大家分享使用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影片教學】
#反例:
set user:666:name jay set user:666:age 18
正例
hmset user:666 name jay age 18
1.3. 為Key設定過期時間,同時注意不同業務的key,盡量過期時間分散一點
如果大量的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條指令的模型:
#使用Pipeline執行了n次指令,整個過程需要1次RTT,模型如下:
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内存。
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的时间复杂度是多少嘛?我们分情况探讨一下:
O(1)
,可以直接del。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()释放锁的时候,可能这把锁已经不属于当前客户端,会解除他人加的锁。
一般也是用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的master节点上拿到了锁,但是加锁的key还没同步到slave节点。恰好这时,master节点发生故障,一个slave节点就会升级为master节点。线程二就可以获取同个key的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。
针对单机问题,可以使用Redlock算法。有兴趣的朋友可以看下我这篇文章哈,七种方案!探讨Redis分布式锁的正确使用姿势
3.2 缓存一致性注意点
有兴趣的朋友,可以看下我这篇文章哈:并发环境下,先操作数据库还是先操作缓存?
3.3 合理评估Redis容量,避免由于频繁set覆盖,导致之前设置的过期时间无效。
我们知道,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 缓存穿透问题
先来看一个常见的缓存使用方式:读请求来了,先查下缓存,缓存有值命中,就直接返回;缓存没命中,就去查数据库,然后把数据库的值更新到缓存,再返回。
快取穿透:指查詢一個一定不存在的數據,由於快取是不命中時需要從資料庫查詢,查不到資料則不寫入緩存,這將導致這個不存在的資料每次請求都要到資料庫去查詢,進而給資料庫帶來壓力。
通俗點說,讀取請求存取時,快取和資料庫都沒有某個值,這樣就會導致每次對這個值的查詢請求都會穿透到資料庫,這就是快取穿透。
快取穿透一般都是這幾種情況產生的:
如何避免快取穿透呢? 一般有三種方法。
布林過濾器原理:它由初始值為0的點陣圖數組和N個雜湊函數組成。一個對一個key進行N個hash演算法取得N個值,在位元數組中將這N個值散列後設定為1,然後查的時候如果特定的這幾個位置都為1,那麼布隆過濾器判斷該key存在。
3.5 快取雪奔問題
#快取雪奔: 指快取中資料大批量到過期時間,而查詢資料量龐大,請求都直接存取資料庫,造成資料庫壓力過大甚至down機。
3.6 快取擊穿問題
快取擊穿: 指熱點key在某個時間點過期的時候,而剛好在這個時間點對這個Key有大量的並發請求過來,從而大量的請求打到db。
快取擊穿看著有點像,其實它兩區別是,快取雪奔是指資料庫壓力過大甚至down機,快取擊穿只是大量並發請求到了DB資料庫層面。可以認為擊穿是緩存雪奔的子集吧。有些文章認為它倆區別,是區別在於擊穿針對某一熱點key緩存,雪奔則是很多key。
解決方案有兩種:
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 使用長連接而不是短連接,並且合理配置客戶端的連接池
4.2 只使用db0
#Redis-standalone架構禁止使用非db0.原因有兩個
#4.3 設定maxmemory 恰當的淘汰策略。
為了防止記憶體積壓膨脹。例如有些時候,業務量大起來了,redis的key被大量使用,內存直接不夠了,維運小哥哥也忘了加大內存了。難道redis直接這樣掛掉?所以要依照實際業務,選好maxmemory-policy(最大記憶體淘汰策略),設定好過期時間。總共有8種記憶體淘汰策略:
4.4 開啟lazy-free 機制
Redis4.0 版本支援lazy-free機制,如果你的Redis還是有bigKey這種玩意存在,建議把lazy-free開啟。當開啟它後,Redis 如果刪除一個 bigkey 時,釋放記憶體的耗時操作,會放到後台執行緒去執行,減少對主執行緒的阻塞影響。
更多程式相關知識,請造訪:程式設計影片! !
以上是21個使用Redis時必須注意的重點(總結)的詳細內容。更多資訊請關注PHP中文網其他相關文章!