Redis中Scan命令踩坑記

1

本來覺得本身對redis命令還蠻熟悉的,各類數據模型各類基於redis的騷操做。可是最近在使用redis的scan的命令式卻踩了一個坑,頓時發覺本身原來對redis的遊標理解的頗有限。因此記錄下這個踩坑的過程,背景以下:redis

公司由於redis服務器內存吃緊,須要刪除一些無用的沒有設置過時時間的key。大概有500多w的key。雖然key的數目聽起來挺嚇人。可是本身玩redis也有年頭了,這種事還不是手到擒來?shell

當時想了下,具體方案是經過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命令,就是用於增量迭代的。這個命令能夠每次返回少許的元素,因此這個命令十分適合用來處理大的數據集的迭代,能夠用於生產環境。ui

scan命令會返回一個數組,第一項爲遊標的位置,第二項是key的列表。若是遊標到達了末尾,第一項會返回0。lua



2

因此我寫的初版的lua腳本以下:spa

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了20w的測試數據:線程

eval "for i = 1, 200000 do redis.call('SET','authToken_' .. i,i) end" 0
複製代碼

而後執行script load命令上傳lua腳本獲得SHA值,而後執行evalsha去執行獲得的SHA值來運行。具體過程以下:翻譯

我每刪1w數據,執行下dbsize(由於這是我本地的redis,裏面只有mock的數據,dbsize也就等同於這個前綴key的數量了)。

奇怪的是,前面幾行都是正常的。可是到了第三次的時候,dbsize變成了16999,多刪了1個,我也沒太在乎,可是最後在dbsize還剩下124204個的時候,數量就不動了。以後不管再執行多少遍,數量還依舊是124204個。

隨即我直接運行scan命令:

發現遊標雖然沒有到達末尾,可是key的列表倒是空的。

這個結果讓我懵逼了一段時間。我仔細檢查了lua腳本,沒有問題啊。難道是redis的scan命令有bug?難道我理解的有問題?

我再去翻看redis的命令文檔對count選項的解釋:

通過詳細研讀,發現count選項所指定的返回數量還不是必定的,雖然知道多是count的問題,但無奈文檔的解釋實在難以很通俗的理解,依舊不知道具體問題在哪



3

後來通過某個小夥伴的提示,看到了另一篇對於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
複製代碼

在本地上傳後執行:

能夠看到,scan命令無法徹底保證每次篩選的數量徹底等同於給定的count,可是整個迭代卻很好的延續下去了。最後也獲得了遊標返回0,也就是到了末尾。至此,測試數據20w被所有刪完。

這段lua只要在套上shell進行循環就能夠直接在生產上跑了。通過估算大概在12分鐘左右能刪除掉500w的數據。

知其然,知其因此然。雖然scan命令之前也曾玩過。可是的確不知道其中的細節。何況文檔的翻譯也不是那麼的準確,以致於本身在面對錯誤的結果時整整浪費了近1個多小時的時間。記錄下來,加深理解。

聯繫做者

相關文章
相關標籤/搜索