《Redis性能問題排查解決手冊》
《Redis的基本操作以及info命令》
《redis object命令》
《清理 redis 死鍵》
參考:
場景:
在這類項目運行久了之后,一些老的key會不斷在redis里積壓,導致redis內存越來越高,對redis的使用效率產生影響,因此需要對於redis數據進行定期清理。
1、死key(死鍵)
所謂死鍵,在redis里有兩個定義:
A、redis中為key設置了過期時間,但是沒有在內存中被實際刪除的key。【 這種情況是可能發生的,在redis中過期鍵的刪除有兩種策略,一般采用的是定期刪除(比如每s刪除10個),這樣的話,如果我們過期鍵產生的速度是大於刪除的速度,則會產生死鍵。】
B、未設置ttl的key(需要根據業務需求,查看能否刪除)
C、死鍵是指在redis中長期未被訪問的key(需要根據業務需求,查看能否刪除)
2、輪轉時間
輪轉時間即idletime,是指該key有多長時間沒有被訪問過(單位 s)。
3、相關命令
其他命令
SSCAN 命令用於迭代集合鍵中的元素。
HSCAN 命令用於迭代哈希鍵中的鍵值對。
ZSCAN 命令用於迭代有序集合中的元素(包括元素成員和元素分值)。
# SCAN 命令是一個基於游標的迭代器(cursor based iterator):SCAN 命令每次被調用之后,都會向用戶返回一個新的游標,用戶在下次迭代時需要使用這個新游標作為 SCAN 命令的游標參數,以此來延續之前的迭代過程。
# 注意:當 SCAN 命令的游標參數被設置為 0 時,服務器將開始一次新的迭代,而當服務器向用戶返回值為 0 的游標時,表示迭代已結束!
3.1、scan語法
redis Scan 命令基本語法如下:
SCAN cursor [MATCH pattern] [COUNT count]
- cursor - 游標。
- pattern - 匹配的模式。
- count - 指定從數據集里返回多少元素,默認值為 10 。
SCAN命令是一個基於游標的迭代器。這意味着命令每次被調用都需要使用上一次這個調用返回的游標作為該次調用的游標參數,以此來延續之前的迭代過程。
示例說明:
這里使用scan 0 match key1111* count 20命令來完成這個查詢,稍顯意外的是,使用一開始都沒有查詢到結果,這個要從scan命令的原理來看。
第一行:scan在遍歷key的時候,0就代表第一次,key1111*代表按照key1111開頭的模式匹配,count 20中的20並不是代表輸出符合條件的key,而是限定服務器單次遍歷的字典槽位數量(約等於)。
那么,什么又叫做槽的數據?這個槽是不是Redis集群中的slot?答案是否定的。其實上圖已經給出了答案了。
如果上面說的“字典槽”的數量是集群中的slot,又知道集群中的slot數量是16384,那么遍歷16384個槽之后,必然能遍歷出來所有的key信息,
上面清楚地看到,當遍歷的字典槽的數量20000的時候,游標依舊沒有走完遍歷結果,因此這個字典槽並不等於集群中的slot的概念。
經過測試,在scan的時候,究竟遍歷多大的COUNT值能完全match到符合條件的key,跟具體對象的key的個數有關,
如果以超過key個數的count來scan,必定會一次性就查找到所有符合條件的key,比如在key個數為10W個的情況下,一次遍歷20w個字典槽,肯定能完全遍歷出來結果。
scan 指令是一系列指令,除了可以遍歷所有的 key 之外,還可以對指定的容器集合進行遍歷。
3.2、zscan 遍歷 zset 集合元素
語法
redis Zscan 命令基本語法如下:
redis 127.0.0.1:6379> ZSCAN key cursor [MATCH pattern] [COUNT count]
- cursor - 游標。
- pattern - 匹配的模式。
- count - 指定從數據集里返回多少元素,默認值為 10 。
可用版本:>= 2.8.0
返回值:返回的每個元素都是一個有序集合元素,一個有序集合元素由一個成員(member)和一個分值(score)組成。
示例:
> ZADD site 1 "Google" 2 "Runoob" 3 "Taobao" 4 "Weibo" (integer) 4 > ZSCAN site 0 match "R*" 1) "0" 2) 1) "Runoob" 2) 2.0
3.3、hscan 遍歷 hash 字典的元素
語法
redis Sscan 命令基本語法如下:
SSCAN key cursor [MATCH pattern] [COUNT count]
- cursor - 游標。
- pattern - 匹配的模式,match要匹配的是field字段不是value。
- count - 指定從數據集里返回多少元素,默認值為 10 。
可用版本:>= 2.8.0
返回值:返回的每個元素都是一個元組,每一個元組元素由一個字段(field) 和值(value)組成。
示例:
> HMSET sites google "google.com" runoob "runoob.com" weibo "weibo.com" 4 "taobao.com" OK > HSCAN sites 0 match "run*" 1) "0" 2) 1) "runoob" 2) "runoob.com"
> hscan sites 0 match "t*"
1) "0"
2)
>hscan sites 0 match "4*"
1) "0"
2) 1) "4"
2) "taobao.com"
3.4、sscan 遍歷 set 集合的元素
語法
redis Sscan 命令基本語法如下:
SSCAN key cursor [MATCH pattern] [COUNT count]
- cursor - 游標。
- pattern - 匹配的模式。
- count - 指定從數據集里返回多少元素,默認值為 10 。
可用版本:>= 2.8.0
返回值:數組列表。
示例:
> SADD myset1 "Google" (integer) 1 > SADD myset1 "Runoob" (integer) 1 > SADD myset1 "Taobao" (integer) 1 > SSCAN myset1 0 match R* 1) "0" 2) 1) "Runoob"
SSCAN 命令、 HSCAN 命令和 ZSCAN 命令的第一個參數總是一個數據庫鍵(某個指定的key)。
另外,使用redis desktop manager的時候,當刷新某個庫的時候,控制台自動不斷刷新scan命令,也就知道它在干嘛了。
4、掃描未設置ttl的key
4.1、shell腳本方式
好在redis-cli提供了scan的參數,來執行scan邏輯。redis-cli的用法見:
# vim redis_no_ttl_key.sh
#!/bin/bash # Redis 通過 scan 找出不過期的 key # SCAN 命令是一個基於游標的迭代器(cursor based iterator):SCAN 命令每次被調用之后,都會向用戶返回一個新的游標,用戶在下次迭代時需要使用這個新游標作為 SCAN 命令的游標參數,以此來延續之前的迭代過程。 # 注意:當 SCAN 命令的游標參數被設置為 0 時,服務器將開始一次新的迭代,而當服務器向用戶返回值為 0 的游標時,表示迭代已結束! db_ip=10.100.41.148 # redis 連接IP db_port=6379 # redis 端口 password='IootCdgN05srE' # redis 密碼 cursor=0 # 第一次游標 cnt=100 # 每次迭代的數量 new_cursor=0 # 下一次游標 redis-cli -c -h $db_ip -p $db_port -a $password scan $cursor count $cnt > scan_tmp_result new_cursor=`sed -n '1p' scan_tmp_result` # 獲取下一次游標 sed -n '2,$p' scan_tmp_result > scan_result # 獲取 key cat scan_result |while read line # 循環遍歷所有 key do ttl_result=`redis-cli -c -h $db_ip -p $db_port -a $password ttl $line` # 獲取key過期時間 if [[ $ttl_result == -1 ]];then #if [ $ttl_result -eq -1 ];then # 判斷過期時間,-1 是不過期 echo $line >> no_ttl.log # 追加到指定日志 fi done while [ $cursor -ne $new_cursor ] # 若游標不為0,則證明沒有迭代完所有的key,繼續執行,直至游標為0 do redis-cli -c -h $db_ip -p $db_port -a $password scan $new_cursor count $cnt > scan_tmp_result new_cursor=`sed -n '1p' scan_tmp_result` sed -n '2,$p' scan_tmp_result > scan_result cat scan_result |while read line do ttl_result=`redis-cli -c -h $db_ip -p $db_port -a $password ttl $line` if [[ $ttl_result == -1 ]];then #if [ $ttl_result -eq -1 ];then echo $line >> no_ttl.log fi done done rm -rf scan_tmp_result rm -rf scan_result
只能掃描redis的0號庫。
網上看到lua實現的:
----------------------------------------------------------------------------------------------------
網上看到一個redis的scan的命令式踩了坑的過程,背景如下:
公司因為redis服務器內存吃緊,需要刪除一些無用的沒有設置過期時間的key。大概有500多w的key。雖然key的數目聽起來挺嚇人。但是自己玩redis也有年頭了,這種事還不是手到擒來?
當時想了下,具體方案是通過lua腳本來過濾出500w的key。然后進行刪除動作。lua腳本在redis server上執行,執行速度快,執行一批只需要和redis server建立一次連接。篩選出來key,然后一次刪1w。然后通過shell腳本循環個500次就能刪完所有的。以前通過lua腳本做過類似批量更新的操作,3w一次也是秒級的。基本不會造成redis的阻塞。這樣算起來,10分鍾就能搞定500w的key。
然后,我就開始直接寫lua腳本。首先是篩選。
用過redis的人,肯定知道redis是單線程作業的,肯定不能用keys命令來篩選,因為keys命令會一次性進行全盤搜索,會造成redis的阻塞,從而會影響正常業務的命令執行。
500w數據量的key,只能增量迭代來進行。redis提供了scan命令,就是用於增量迭代的。這個命令可以每次返回少量的元素,所以這個命令十分適合用來處理大的數據集的迭代,可以用於生產環境。
scan命令會返回一個數組,第一項為游標的位置,第二項是key的列表。如果游標到達了末尾,第一項會返回0。
踩坑1:
所以我寫的第一版的lua腳本如下:
local c = 0 local resp = redis.call('SCAN',c,'MATCH','authToken*','COUNT',10000) c = tonumber(resp[1]) local dataList = resp[2] for i=1,#dataList do local d = dataList[i] local ttl = redis.call('TTL',d) if ttl == -1 then redis.call('DEL',d) end end if c==0 then return 'all finished' else return 'end' end
在本地的測試redis環境中,通過執行以下命令mock了1k的測試數據:
在redis的客戶端或console輸入如下命令執行:
測試環境-不要誤刪:0>eval "for i = 1, 1000 do redis.call('SET','authToken_' .. i,i) end" 0 null 測試環境-不要誤刪:0>
成功插入1000個ttl為-1的key-value
然后執行script load命令上傳lua腳本得到SHA值,然后執行evalsha去執行得到的SHA值來運行。具體過程如下:
測試環境-不要誤刪:0>script load "local c = 0 local resp = redis.call('SCAN',c,'MATCH','authToken*','COUNT',10000) c = tonumber(resp[1]) local dataList = resp[2] for i=1,#dataList do local d = dataList[i] local ttl = redis.call('TTL',d) if ttl == -1 then print(d) redis.call('DEL',d) end end if c==0 then return 'all finished' else return 'end' end" "21df2cc32036b90018d5af738759830144ffa28c" 測試環境-不要誤刪:0>
evalsha執行:
測試環境-不要誤刪:0>evalsha 21df2cc32036b90018d5af738759830144ffa28c 0 "all finished" 測試環境-不要誤刪:0>
結果:
我每刪1w數據,執行下dbsize(因為這是我本地的redis,里面只有mock的數據,dbsize也就等同於這個前綴key的數量了)。
奇怪的是,前面幾行都是正常的。但是到了第三次的時候,dbsize變成了16999,多刪了1個,我也沒太在意,但是最后在dbsize還剩下124204個的時候,數量就不動了。之后無論再執行多少遍,數量還依舊是124204個。
隨即我直接運行scan命令:
發現游標雖然沒有到達末尾,但是key的列表卻是空的。
我再去翻看redis的命令文檔對count選項的解釋:
經過詳細研讀,發現count選項所指定的返回數量還不是一定的,雖然知道可能是count的問題,但無奈文檔的解釋實在難以很通俗的理解,依舊不知道具體問題在哪,后來經過某個小伙伴的提示,看到了另外一篇對於scan命令count選項通俗的解釋:
看完之后恍然大悟。原來count選項后面跟的數字並不是意味着每次返回的元素數量,而是scan命令每次遍歷字典槽的數量。我scan執行的時候每一次都是從游標0的位置開始遍歷,而並不是每一個字典槽里都存放着我所需要篩選的數據,這就造成了我最后的一個現象:雖然我count后面跟的是10000,但是實際redis從開頭往下遍歷了10000個字典槽后,發現沒有數據槽存放着我所需要的數據。所以我最后的dbsize數量永遠停留在了124204個。
所以在使用scan命令的時候,如果需要迭代的遍歷,需要每次調用都需要使用上一次這個調用返回的游標作為該次調用的游標參數,以此來延續之前的迭代過程。
可用版本:
至此,心中的疑惑就此解開,改了一版lua:
local c = tonumber(ARGV[1]) local resp = redis.call('SCAN',c,'MATCH','authToken*','COUNT',10000) c = tonumber(resp[1]) local dataList = resp[2] for i=1,#dataList do local d = dataList[i] local ttl = redis.call('TTL',d) if ttl == -1 then redis.call('DEL',d) end end return c
在本地上傳:
測試環境-不要誤刪:0>script load "local c = tonumber(ARGV[1]) local resp = redis.call('SCAN',c,'MATCH','authToken*','COUNT',10000) c = tonumber(resp[1]) local dataList = resp[2] for i=1,#dataList do local d = dataList[i] local ttl = redis.call('TTL',d) if ttl == -1 then redis.call('DEL',d) end end return c" "a9e4f8116fec50c81e0716e4fdb2f0bbdd327f5c" 測試環境-不要誤刪:0>
執行:
測試環境-不要誤刪:0>eval "for i = 1, 200000 do redis.call('SET','authToken_' .. i,i) end" 0 null 測試環境-不要誤刪:0>dbsize "200012" 測試環境-不要誤刪:0>evalsha a9e4f8116fec50c81e0716e4fdb2f0bbdd327f5c 1 0 0 "77104" 測試環境-不要誤刪:0>dbsize "190012" 測試環境-不要誤刪:0>evalsha a9e4f8116fec50c81e0716e4fdb2f0bbdd327f5c 1 0 77104 "185490" 測試環境-不要誤刪:0>
77104的參數是上次evalsha返回的值。
dbsize用於統計刪除后的總記錄數。
一直反復調用直到執行lua腳本返回為0:
可以看到,scan命令沒法完全保證每次篩選的數量完全等同於給定的count,但是整個迭代卻很好的延續下去了。最后也得到了游標返回0,也就是到了末尾。至此,測試數據20w被全部刪完。
這段lua只要在套上shell進行循環就可以直接在生產上跑了。經過估算大概在12分鍾左右能刪除掉500w的數據。
知其然,知其所以然。雖然scan命令以前也曾玩過。但是的確不知道其中的細節。況且文檔的翻譯也不是那么的准確,以至於自己在面對錯誤的結果時整整浪費了近1個多小時的時間。記錄下來,加深理解。
原文鏈接:https://blog.csdn.net/bryan_zhang_31/article/details/105381999
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
項目實現
語言:shell+lua
shell腳本
redisCom="redis-cli -p 3310 -a password"
start=0
fileNamePre="unUseData_"
time=$(date "+%Y-%m-%d %H:%M:%S")
echo "拆分執行 開始時間:${time} 開始索引 ${start}"
data=`${redisCom} --eval getUnusedData2.0.lua , ${start}`
echo ${data} | sed 's/ / \n/g' > "${fileNamePre}""${start}"
start=`echo ${data} | cut -d ' ' -f1`
time=$(date "+%Y-%m-%d %H:%M:%S")
echo "拆分執行 結束時間:${time}"
while(( $start>0 ))
do
time=$(date "+%Y-%m-%d %H:%M:%S")
echo "拆分執行 開始時間:${time} 開始索引 ${start}"
data=`${redisCom} --eval getUnusedData2.0.lua , ${start}`
echo ${data} | sed 's/ / \n/g' > "${fileNamePre}""${start}"
start=`echo ${data} | cut -d ' ' -f1`
time=$(date "+%Y-%m-%d %H:%M:%S")
echo "拆分執行 結束時間:${time}"
done
time=$(date "+%Y-%m-%d %H:%M:%S")
echo "結束時間:${time}"
shell腳本主要是做一個觸發作用,循環調用getUnusedData2.0的LUA腳本。LUA腳本的目的就是每次遍歷10000條數據,找到死鍵,然后做相應處理。返回值如果為0表示redis遍歷完畢,都則繼續進行遍歷。
最后shell將遍歷后的結果進行簡單切割處理后,存儲至文件。
LUA腳本-getUnusedData2.0
-- 獲取在臨界時間外的數據
local function getUnuseData()
data = nextCycle(start, count, match)
start = tonumber(data[1])
--開始判斷
for key,value in pairs(data[2])
do
local curTime = 0
curTime = getIdleTime(value)
if(curTime > maxTime)
then
local delData = {}
delData[1] = value
delData[2] = curTime
delData[3] , delData[4] = getkey(value)
--return delData
--delKey(value)
table.insert(keyData , delData)
--delKey(value)
end
end
return keyData , start
end
具體操作可以查看github源代碼
LUA腳本-createRaw
-- 測試使用 為數據庫創建隨機數據
-- 隨機數
local num = 1000000
local function createRaw(num)
local value = math.random(num)
local key = "create_rew_data_"..tostring(math.random(num))
--local value = math.randomseed(num)
--return value
return redis.call("SET" , key, value)
end
math.randomseed(num)
--return num
--return createRaw(num)
for i=num,1,-1
do
createRaw(num)
end
目的是為了測試,在自己測試庫中寫入隨機數據。
項目運行
生成隨機數據
redis-cli --eval createRawData.lua
生成63萬數據
查找數據
只查找符合要求的數據,因為我們數據都是新生成的,所以我們設置閾值時間為0,一次查找10000條數據。
修改如下:
運行腳本 耗時16s
查找並落地數據+刪除
刪掉注釋,開始刪除數據
運行程序,用時26s 原因(生成數據value均為數字,存儲很快):
————————————————
版權聲明:本文為CSDN博主「genglintong」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_31794773/article/details/82831024
1、需要清理的keys。
未設置ttl的keys,且半年內無訪問。
2、redis 刪除過期建策略。
Redis刪除過期鍵有兩種策略:passive way和active way
- passive way(惰性刪除):當客戶端訪問到過期鍵時,發現它已過期,Redis會主動刪除它
- active way(定期刪除):Redis會定期調用刪除過期鍵,調用頻率由參數hz控制,默認每秒調用10次
3、獲取keys的idletime
127.0.0.1:6379> set aa bb OK 127.0.0.1:6379> object idletime aa (integer) 12 127.0.0.1:6379> get aa # 訪問后,idletime會清零 "bb" 127.0.0.1:6379> object idletime aa (integer) 2
4、清理過期的idletime腳本
#!/bin/bash file_dir="/data/tmp" redis-cli -h jkb-hw-prod-apple1 -p 6391 --scan >$file_dir/$port.keys ruleTime='12960000' while read line do storageTime=$(redis-cli -h jkb-hw-prod-apple1 -p 6391 OBJECT IDLETIME ${line}|awk '{print $1}') if [ ! "${storageTime}x" == "x" ];then if [ ${storageTime} -gt ${ruleTime} ];then redis-cli -h jkb-hw-prod-apple1 -p 6391 del ${delKey} fi fi echo "${rowProcessed}" >/data/tmp/record-number.txt done <$file_dir/$port.keys
github地址這個項目是對於redis中的key進行篩選,查找到輪轉時間(長期沒有使用的時間)大於某個閾值的key,並將它做一些清理落地處理。
參考:
https://www.cnblogs.com/sunshine-long/p/12582681.html