Redis有哪些慢操作?
Redis是否變慢了?
從業(yè)務(wù)服務(wù)器到Redis服務(wù)器這條調(diào)用鏈路中變慢的原因可能有2個(gè)
- 業(yè)務(wù)服務(wù)器到Redis服務(wù)器之間出現(xiàn)了網(wǎng)絡(luò)問(wèn)題,例如網(wǎng)絡(luò)丟包,延遲比較嚴(yán)重
- Redis本身的執(zhí)行出現(xiàn)問(wèn)題,此時(shí)我們就需要排查Redis的問(wèn)題
但是大多數(shù)情況下都是Redis服務(wù)的問(wèn)題。但是應(yīng)該如何衡量Redis變慢了呢?命令執(zhí)行時(shí)間大于1s,大于2s?這其實(shí)并沒(méi)有一個(gè)固定的標(biāo)準(zhǔn)。
例如在一個(gè)配置較高的服務(wù)器中,0.5毫秒就認(rèn)為Redis變慢了,在一個(gè)配置較低的服務(wù)器中,3毫秒才認(rèn)為Redis變慢了。所以我們要針對(duì)自己的機(jī)器做基準(zhǔn)測(cè)試,看平常情況下Redis處理命令的時(shí)間是多長(zhǎng)?
我們可以使用如下命令來(lái)監(jiān)測(cè)和統(tǒng)計(jì)測(cè)試期間的最大延遲(以微秒為單位)
redis-cli --latency -h `host` -p `port`
比如執(zhí)行如下命令
[root@VM-0-14-centos src]# ./redis-cli -h 127.0.0.1 -p 6379 --intrinsic-latency 60
Max latency so far: 1 microseconds.
Max latency so far: 12 microseconds.
Max latency so far: 55 microseconds.
Max latency so far: 124 microseconds.
Max latency so far: 133 microseconds.
Max latency so far: 142 microseconds.
Max latency so far: 982 microseconds.
Max latency so far: 1049 microseconds.
Max latency so far: 2366 microseconds.
Max latency so far: 3725 microseconds.
52881684 total runs (avg latency: 1.1346 microseconds / 1134.61 nanoseconds per run).
Worst run took 3283x longer than the average latency.
參數(shù)中的60是測(cè)試執(zhí)行的秒數(shù),可以看到最大延遲為3725微秒(3毫秒左右),如果命令的執(zhí)行遠(yuǎn)超3毫秒,此時(shí)Redis就有可能很慢了!
那么Redis有哪些慢操作呢?
Redis有哪些慢操作?
Redis的各種命令是在一個(gè)線程中依次執(zhí)行的,如果一個(gè)命令在Redis中執(zhí)行的時(shí)間過(guò)長(zhǎng),就會(huì)影響整體的性能,因?yàn)楹竺娴恼?qǐng)求要等到前面的請(qǐng)求被處理完才能被處理,這些耗時(shí)的操作有如下幾個(gè)部分
Redis可以通過(guò)日志記錄那些耗時(shí)長(zhǎng)的命令,使用如下配置即可
# 命令執(zhí)行耗時(shí)超過(guò) 5 毫秒,記錄慢日志
CONFIG SET slowlog-log-slower-than 5000
# 只保留最近 500 條慢日志
CONFIG SET slowlog-max-len 500
執(zhí)行如下命令,就可以查詢到最近記錄的慢日志
127.0.0.1:6379> SLOWLOG get 5
1) 1) (integer) 32693 # 慢日志ID
2) (integer) 1593763337 # 執(zhí)行時(shí)間戳
3) (integer) 5299 # 執(zhí)行耗時(shí)(微秒)
4) 1) "LRANGE" # 具體執(zhí)行的命令和參數(shù)
2) "user_list:2000"
3) "0"
4) "-1"
2) 1) (integer) 32692
2) (integer) 1593763337
3) (integer) 5044
4) 1) "GET"
2) "user_info:1000"
使用復(fù)雜度過(guò)高的命令
之前的文章我們已經(jīng)介紹了Redis的底層數(shù)據(jù)結(jié)構(gòu),它們的時(shí)間復(fù)雜度如下表所示
名稱 時(shí)間復(fù)雜度 dict(字典) O(1) ziplist (壓縮列表) O(n) zskiplist (跳表) O(logN) quicklist(快速列表) O(n) intset(整數(shù)集合) O(n)
「單元素操作」:對(duì)集合中的元素進(jìn)行增刪改查操作和底層數(shù)據(jù)結(jié)構(gòu)相關(guān),如對(duì)字典進(jìn)行增刪改查時(shí)間復(fù)雜度為O(1),對(duì)跳表進(jìn)行增刪查時(shí)間復(fù)雜為O(logN)
「范圍操作」:對(duì)集合進(jìn)行遍歷操作,比如Hash類型的HGETALL,Set類型的SMEMBERS,List類型的LRANGE,ZSet類型的ZRANGE,時(shí)間復(fù)雜度為O(n),避免使用,用SCAN系列命令代替。(hash用hscan,set用sscan,zset用zscan)
「聚合操作」:這類操作的時(shí)間復(fù)雜度通常大于O(n),比如SORT、SUNION、ZUNIONSTORE
「統(tǒng)計(jì)操作」:當(dāng)想獲取集合中的元素個(gè)數(shù)時(shí),如LLEN或者SCARD,時(shí)間復(fù)雜度為O(1),因?yàn)樗鼈兊牡讓訑?shù)據(jù)結(jié)構(gòu)如quicklist,dict,intset保存了元素的個(gè)數(shù)
「邊界操作」:list底層是用quicklist實(shí)現(xiàn)的,quicklist保存了鏈表的頭尾節(jié)點(diǎn),因此對(duì)鏈表的頭尾節(jié)點(diǎn)進(jìn)行操作,時(shí)間復(fù)雜度為O(1),如LPOP、RPOP、LPUSH、RPUSH
「當(dāng)想獲取Redis中的key時(shí),避免使用keys *」 ,Redis中保存的鍵值對(duì)是保存在一個(gè)字典中的(和Java中的HashMap類似,也是通過(guò)數(shù)組+鏈表的方式實(shí)現(xiàn)的),key的類型都是string,value的類型可以是string,set,list等
例如當(dāng)我們執(zhí)行如下命令后,redis的字典結(jié)構(gòu)如下
set bookName redis;
rpush fruits banana apple;
我們可以用keys命令來(lái)查詢Redis中特定的key,如下所示
# 查詢所有的key
keys *
# 查詢以book為前綴的key
keys book*
keys命令的復(fù)雜度是O(n),它會(huì)遍歷這個(gè)dict中的所有key,如果Redis中存的key非常多,所有讀寫Redis的指令都會(huì)被延遲等待,所以千萬(wàn)不用在生產(chǎn)環(huán)境用這個(gè)命令(如果你已經(jīng)準(zhǔn)備離職的話,祝你玩的開(kāi)心)。
「既然不讓你用keys,肯定有替代品,那就是scan」
scan是通過(guò)游標(biāo)逐步遍歷的,因此不會(huì)長(zhǎng)時(shí)間阻塞Redis
「用用zscan遍歷zset,hscan遍歷hash,sscan遍歷set的原理和scan命令類似,因?yàn)閔ash,set,zset的底層實(shí)現(xiàn)的數(shù)據(jù)結(jié)構(gòu)中都有dict?!?/p>
操作bigkey
「如果一個(gè)key對(duì)應(yīng)的value非常大,那么這個(gè)key就被稱為bigkey。寫入bigkey在分配內(nèi)存時(shí)需要消耗更長(zhǎng)的時(shí)間。同樣,刪除bigkey釋放內(nèi)存也需要消耗更長(zhǎng)的時(shí)間」
如果在慢日志中發(fā)現(xiàn)了SET/DEL這種復(fù)雜度不高的命令,此時(shí)你就應(yīng)該排查一下是否是由于寫入bigkey導(dǎo)致的。
「如何定位bigkey?」
Redis提供了掃描bigkey的命令
$ redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.01
-------- summary -------
Sampled 829675 keys in the keyspace!
Total key length in bytes is 10059825 (avg len 12.13)
Biggest string found 'key:291880' has 10 bytes
Biggest list found 'mylist:004' has 40 items
Biggest set found 'myset:2386' has 38 members
Biggest hash found 'myhash:3574' has 37 fields
Biggest zset found 'myzset:2704' has 42 members
36313 strings with 363130 bytes (04.38% of keys, avg size 10.00)
787393 lists with 896540 items (94.90% of keys, avg size 1.14)
1994 sets with 40052 members (00.24% of keys, avg size 20.09)
1990 hashs with 39632 fields (00.24% of keys, avg size 19.92)
1985 zsets with 39750 members (00.24% of keys, avg size 20.03)
可以看到命令的輸入有如下3個(gè)部分
- 內(nèi)存中key的數(shù)量,已經(jīng)占用的總內(nèi)存,每個(gè)key占用的平均內(nèi)存
- 每種類型占用的最大內(nèi)存,已經(jīng)key的名字
- 每種數(shù)據(jù)類型的占比,以及平均大小
這個(gè)命令的原理就是redis在內(nèi)部執(zhí)行了scan命令,遍歷實(shí)例中所有的key,然后正對(duì)key的類型,分別執(zhí)行strlen,llen,hlen,scard,zcard命令,來(lái)獲取string類型的長(zhǎng)度,容器類型(list,hash,set,zset)的元素個(gè)數(shù)
使用這個(gè)命令需要注意如下兩個(gè)問(wèn)題
- 對(duì)線上實(shí)例進(jìn)行bigkey掃描時(shí),為避免ops(operation per second 每秒操作次數(shù))突增,可以通過(guò)-i增加一個(gè)休眠參數(shù),上面的含義為,每隔100條scan指令就會(huì)休眠0.01s
- 對(duì)于容器類型(list,hash,set,zset),掃描出的是元素最多的key,但一個(gè)key的元素?cái)?shù)量多,不一定代表占用的內(nèi)存多
「如何解決bigkey帶來(lái)的性能問(wèn)題?」
- 盡量避免寫入bigkey
- 如果使用的是redis4.0以上版本,可以用unlink命令代替del,此命令可以把釋放key內(nèi)存的操作,放到后臺(tái)線程中去執(zhí)行
- 如果使用的是redis6.0以上版本,可以開(kāi)啟lazy-free機(jī)制(lazyfree-lazy-user-del yes),執(zhí)行del命令的時(shí)候,也會(huì)放到后臺(tái)線程中去執(zhí)行
大量key集中過(guò)期
我們可以給Redis中的key設(shè)置過(guò)期時(shí)間,那么當(dāng)key過(guò)期了,它在什么時(shí)候會(huì)被刪除呢?
「如果讓我們寫Redis過(guò)期策略,我們會(huì)想到如下三種方案」
- 定時(shí)刪除,在設(shè)置鍵的過(guò)期時(shí)間的同時(shí),創(chuàng)建一個(gè)定時(shí)器。當(dāng)鍵的過(guò)期時(shí)間來(lái)臨時(shí),立即執(zhí)行對(duì)鍵的刪除操作
- 惰性刪除,每次獲取鍵的時(shí)候,判斷鍵是否過(guò)期,如果過(guò)期的話,就刪除該鍵,如果沒(méi)有過(guò)期,則返回該鍵
- 定期刪除,每隔一段時(shí)間,對(duì)鍵進(jìn)行一次檢查,刪除里面的過(guò)期鍵 定時(shí)刪除策略對(duì)CPU不友好,當(dāng)過(guò)期鍵比較多的時(shí)候,Redis線程用來(lái)刪除過(guò)期鍵,會(huì)影響正常請(qǐng)求的響應(yīng)
定時(shí)刪除策略對(duì)CPU不友好,當(dāng)過(guò)期鍵比較多的時(shí)候,Redis線程用來(lái)刪除過(guò)期鍵,會(huì)影響正常請(qǐng)求的響應(yīng)
惰性刪除讀CPU是比較有好的,但是會(huì)浪費(fèi)大量的內(nèi)存。如果一個(gè)key設(shè)置過(guò)期時(shí)間放到內(nèi)存中,但是沒(méi)有被訪問(wèn)到,那么它會(huì)一直存在內(nèi)存中
定期刪除策略則對(duì)CPU和內(nèi)存都比較友好
redis過(guò)期key的刪除策略選擇了如下兩種
- 惰性刪除
- 定期刪除
「惰性刪除」 客戶端在訪問(wèn)key的時(shí)候,對(duì)key的過(guò)期時(shí)間進(jìn)行校驗(yàn),如果過(guò)期了就立即刪除
「定期刪除」 Redis會(huì)將設(shè)置了過(guò)期時(shí)間的key放在一個(gè)獨(dú)立的字典中,定時(shí)遍歷這個(gè)字典來(lái)刪除過(guò)期的key,遍歷策略如下
- 每秒進(jìn)行10次過(guò)期掃描,每次從過(guò)期字典中隨機(jī)選出20個(gè)key
- 刪除20個(gè)key中已經(jīng)過(guò)期的key
- 如果過(guò)期key的比例超過(guò)1/4,則進(jìn)行步驟一
- 每次掃描時(shí)間的上限默認(rèn)不超過(guò)25ms,避免線程卡死
「因?yàn)镽edis中過(guò)期的key是由主線程刪除的,為了不阻塞用戶的請(qǐng)求,所以刪除過(guò)期key的時(shí)候是少量多次」。源碼可以參考expire.c中的activeExpireCycle方法
- 為了避免主線程一直在刪除key,我們可以采用如下兩種方案
- 給同時(shí)過(guò)期的key增加一個(gè)隨機(jī)數(shù),打散過(guò)期時(shí)間,降低清除key的壓力
如果你使用的是redis4.0版本以上的redis,可以開(kāi)啟lazy-free機(jī)制(lazyfree-lazy-expire yes),當(dāng)刪除過(guò)期key時(shí),把釋放內(nèi)存的操作放到后臺(tái)線程中執(zhí)行
內(nèi)存達(dá)到上限,觸發(fā)淘汰策略
Redis是一個(gè)內(nèi)存數(shù)據(jù)庫(kù),當(dāng)Redis使用的內(nèi)存超過(guò)物理內(nèi)存的限制后,內(nèi)存數(shù)據(jù)會(huì)和磁盤產(chǎn)生頻繁的交換,交換會(huì)導(dǎo)致Redis性能急劇下降。所以在生產(chǎn)環(huán)境中我們通過(guò)配置參數(shù)maxmemoey來(lái)限制使用的內(nèi)存大小。
當(dāng)實(shí)際使用的內(nèi)存超過(guò)maxmemoey后,Redis提供了如下幾種可選策略。
「Redis的淘汰策略也是在主線程中執(zhí)行的。但內(nèi)存超過(guò)Redis上限后,每次寫入都需要淘汰一些key,導(dǎo)致請(qǐng)求時(shí)間變長(zhǎng)」
可以通過(guò)如下幾個(gè)方式進(jìn)行改善
- 增加內(nèi)存或者將數(shù)據(jù)放到多個(gè)實(shí)例中
- 淘汰策略改為隨機(jī)淘汰,一般來(lái)說(shuō)隨機(jī)淘汰比lru快很多
- 避免存儲(chǔ)bigkey,降低釋放內(nèi)存的耗時(shí)
寫AOF日志的方式為always
Redis的持久化機(jī)制有RDB快照和AOF日志,每次寫命令之后后,Redis提供了如下三種刷盤機(jī)制
「當(dāng)aof的刷盤機(jī)制為always,redis每處理一次寫命令,都會(huì)把寫命令刷到磁盤中才返回,整個(gè)過(guò)程是在Redis主線程中進(jìn)行的,勢(shì)必會(huì)拖慢redis的性能」
當(dāng)aof的刷盤機(jī)制為everysec,redis寫完內(nèi)存后就返回,刷盤操作是放到后臺(tái)線程中去執(zhí)行的,后臺(tái)線程每隔1秒把內(nèi)存中的數(shù)據(jù)刷到磁盤中
當(dāng)aof的刷盤機(jī)制為no,宕機(jī)后可能會(huì)造成部分?jǐn)?shù)據(jù)丟失,一般不采用。
「一般情況下,aof刷盤機(jī)制配置為everysec即可」
fork耗時(shí)過(guò)長(zhǎng)
在持久化一節(jié)中,我們已經(jīng)提到「Redis生成rdb文件和aof日志重寫,都是通過(guò)主線程fork子進(jìn)程的方式,讓子進(jìn)程來(lái)執(zhí)行的,主線程的內(nèi)存越大,阻塞時(shí)間越長(zhǎng)?!?/p>
可以通過(guò)如下方式優(yōu)化
- 控制Redis實(shí)例的內(nèi)存大小,盡量控制到10g以內(nèi),因?yàn)閮?nèi)存越大,阻塞時(shí)間越長(zhǎng)
- 配置合理的持久化策略,如在slave節(jié)點(diǎn)生成rdb快照
使用swap分區(qū)
當(dāng)機(jī)器的內(nèi)存不夠時(shí),操作系統(tǒng)會(huì)將部分內(nèi)存的數(shù)據(jù)置換到磁盤上,這塊磁盤區(qū)域就是Swap分區(qū),當(dāng)應(yīng)用程序再次訪問(wèn)這些數(shù)據(jù)的時(shí)候,就需要從磁盤上讀取,導(dǎo)致性能嚴(yán)重下降
「當(dāng)Redis性能急劇下降時(shí)就有可能是數(shù)據(jù)被換到Swap分區(qū),我們?cè)撊绾闻挪镽edis數(shù)據(jù)是否被換到Swap分區(qū)呢?」
# 先找到redis-server的進(jìn)程id
ps -ef | grep redis-server
# 查看redis swap的使用情況
cat /proc/$pid/smaps | egrep '^(Swap|Size)'
[root@VM-0-14-centos ~]# cat /proc/2370/smaps | egrep '^(Swap|Size)'
Size: 1568 kB
Swap: 0 kB
Size: 8 kB
Swap: 0 kB
Size: 24 kB
Swap: 0 kB
Size: 2200 kB
Swap: 0 kB
每一行Size表示Redis所用的一塊內(nèi)存大小,Size下面的Swap表示這塊大小的內(nèi)存,有多少已經(jīng)被換到磁盤上了,如果這2個(gè)值相等,說(shuō)明這塊內(nèi)存的數(shù)據(jù)都已經(jīng)被換到磁盤上了
我們可以通過(guò)如下方式來(lái)解決
- 增加機(jī)器內(nèi)存
- 整理內(nèi)存碎片
最后我們總結(jié)一下Redis的慢操作