<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Redis 中 scan 命令踩坑,千萬(wàn)別亂用?。?/h1>

          共 3346字,需瀏覽 7分鐘

           ·

          2021-05-10 15:55

          點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)

          作者:鉑賽東
          鏈接:www.jianshu.com/p/8cf8aac3dc25

          1

          原本以為自己對(duì)redis命令還蠻熟悉的,各種數(shù)據(jù)模型各種基于redis的騷操作。但是最近在使用redis的scan的命令式卻踩了一個(gè)坑,頓時(shí)發(fā)覺(jué)自己原來(lái)對(duì)redis的游標(biāo)理解的很有限。

          所以記錄下這個(gè)踩坑的過(guò)程,背景如下:

          公司因?yàn)閞edis服務(wù)器內(nèi)存吃緊,需要?jiǎng)h除一些無(wú)用的沒(méi)有設(shè)置過(guò)期時(shí)間的key。大概有500多w的key。雖然key的數(shù)目聽起來(lái)挺嚇人。但是自己玩redis也有年頭了,這種事還不是手到擒來(lái)?

          當(dāng)時(shí)想了下,具體方案是通過(guò)lua腳本來(lái)過(guò)濾出500w的key。然后進(jìn)行刪除動(dòng)作。lua腳本在redis server上執(zhí)行,執(zhí)行速度快,執(zhí)行一批只需要和redis server建立一次連接。篩選出來(lái)key,然后一次刪1w。然后通過(guò)shell腳本循環(huán)個(gè)500次就能刪完所有的。以前通過(guò)lua腳本做過(guò)類似批量更新的操作,3w一次也是秒級(jí)的?;静粫?huì)造成redis的阻塞。這樣算起來(lái),10分鐘就能搞定500w的key。

          然后,我就開始直接寫lua腳本。首先是篩選。

          用過(guò)redis的人,肯定知道redis是單線程作業(yè)的,肯定不能用keys命令來(lái)篩選,因?yàn)閗eys命令會(huì)一次性進(jìn)行全盤搜索,會(huì)造成redis的阻塞,從而會(huì)影響正常業(yè)務(wù)的命令執(zhí)行。

          500w數(shù)據(jù)量的key,只能增量迭代來(lái)進(jìn)行。redis提供了scan命令,就是用于增量迭代的。這個(gè)命令可以每次返回少量的元素,所以這個(gè)命令十分適合用來(lái)處理大的數(shù)據(jù)集的迭代,可以用于生產(chǎn)環(huán)境。

          scan命令會(huì)返回一個(gè)數(shù)組,第一項(xiàng)為游標(biāo)的位置,第二項(xiàng)是key的列表。如果游標(biāo)到達(dá)了末尾,第一項(xiàng)會(huì)返回0。

          2

          所以我寫的第一版的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

          在本地的測(cè)試redis環(huán)境中,通過(guò)執(zhí)行以下命令mock了20w的測(cè)試數(shù)據(jù):

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

          然后執(zhí)行script load命令上傳lua腳本得到SHA值,然后執(zhí)行evalsha去執(zhí)行得到的SHA值來(lái)運(yùn)行。具體過(guò)程如下:

          我每刪1w數(shù)據(jù),執(zhí)行下dbsize(因?yàn)檫@是我本地的redis,里面只有mock的數(shù)據(jù),dbsize也就等同于這個(gè)前綴key的數(shù)量了)。

          奇怪的是,前面幾行都是正常的。但是到了第三次的時(shí)候,dbsize變成了16999,多刪了1個(gè),我也沒(méi)太在意,但是最后在dbsize還剩下124204個(gè)的時(shí)候,數(shù)量就不動(dòng)了。之后無(wú)論再執(zhí)行多少遍,數(shù)量還依舊是124204個(gè)。

          隨即我直接運(yùn)行scan命令:

          發(fā)現(xiàn)游標(biāo)雖然沒(méi)有到達(dá)末尾,但是key的列表卻是空的。

          這個(gè)結(jié)果讓我懵逼了一段時(shí)間。我仔細(xì)檢查了lua腳本,沒(méi)有問(wèn)題啊。難道是redis的scan命令有bug?難道我理解的有問(wèn)題?

          我再去翻看redis的命令文檔對(duì)count選項(xiàng)的解釋:

          經(jīng)過(guò)詳細(xì)研讀,發(fā)現(xiàn)count選項(xiàng)所指定的返回?cái)?shù)量還不是一定的,雖然知道可能是count的問(wèn)題,但無(wú)奈文檔的解釋實(shí)在難以很通俗的理解,依舊不知道具體問(wèn)題在哪。關(guān)注公眾號(hào)Java技術(shù)棧,在后臺(tái)回復(fù):面試,可以獲取我整理的 Redis 系列面試題和答案,非常齊全。

          3

          后來(lái)經(jīng)過(guò)某個(gè)小伙伴的提示,看到了另外一篇對(duì)于scan命令count選項(xiàng)通俗的解釋:

          看完之后恍然大悟。原來(lái)count選項(xiàng)后面跟的數(shù)字并不是意味著每次返回的元素?cái)?shù)量,而是scan命令每次遍歷字典槽的數(shù)量

          我scan執(zhí)行的時(shí)候每一次都是從游標(biāo)0的位置開始遍歷,而并不是每一個(gè)字典槽里都存放著我所需要篩選的數(shù)據(jù),這就造成了我最后的一個(gè)現(xiàn)象:雖然我count后面跟的是10000,但是實(shí)際redis從開頭往下遍歷了10000個(gè)字典槽后,發(fā)現(xiàn)沒(méi)有數(shù)據(jù)槽存放著我所需要的數(shù)據(jù)。所以我最后的dbsize數(shù)量永遠(yuǎn)停留在了124204個(gè)。

          所以在使用scan命令的時(shí)候,如果需要迭代的遍歷,需要每次調(diào)用都需要使用上一次這個(gè)調(diào)用返回的游標(biāo)作為該次調(diào)用的游標(biāo)參數(shù),以此來(lái)延續(xù)之前的迭代過(guò)程。

          至此,心中的疑惑就此解開,改了一版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

          在本地上傳后執(zhí)行:

          可以看到,scan命令沒(méi)法完全保證每次篩選的數(shù)量完全等同于給定的count,但是整個(gè)迭代卻很好的延續(xù)下去了。最后也得到了游標(biāo)返回0,也就是到了末尾。至此,測(cè)試數(shù)據(jù)20w被全部刪完。

          這段lua只要在套上shell進(jìn)行循環(huán)就可以直接在生產(chǎn)上跑了。經(jīng)過(guò)估算大概在12分鐘左右能刪除掉500w的數(shù)據(jù)。

          知其然,知其所以然。雖然scan命令以前也曾玩過(guò)。但是的確不知道其中的細(xì)節(jié)。況且文檔的翻譯也不是那么的準(zhǔn)確,以至于自己在面對(duì)錯(cuò)誤的結(jié)果時(shí)整整浪費(fèi)了近1個(gè)多小時(shí)的時(shí)間。記錄下來(lái),加深理解。

          最后,關(guān)注公眾號(hào)Java技術(shù)棧,在后臺(tái)回復(fù):面試,可以獲取我整理的 Redis 系列面試題和答案,非常齊全。






          關(guān)注Java技術(shù)??锤喔韶?/strong>



          獲取 Spring Boot 實(shí)戰(zhàn)筆記!
          瀏覽 48
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)

          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费的鸡巴网站 | 日韩性爱三级片在线观看 | 亚洲无码专区精品 | 国产无码在线诱惑视频 | 中国一级黄片AV |