本来觉得本身对redis命令还蛮熟悉的,各类数据模型各类基于redis的骚操做。可是最近在使用redis的scan的命令式却踩了一个坑,顿时发觉本身原来对redis的游标理解的颇有限。redis
因此记录下这个踩坑的过程,背景以下:shell
公司由于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的阻塞,从而会影响正常业务的命令执行。lua
500w数据量的key,只能增量迭代来进行。redis提供了scan命令,就是用于增量迭代的。这个命令能够每次返回少许的元素,因此这个命令十分适合用来处理大的数据集的迭代,能够用于生产环境。spa
scan命令会返回一个数组,第一项为游标的位置,第二项是key的列表。若是游标到达了末尾,第一项会返回0。线程
因此我写的初版的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了20w的测试数据:3d
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的问题,但无奈文档的解释实在难以很通俗的理解,依旧不知道具体问题在哪。
后来通过某个小伙伴的提示,看到了另一篇对于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个多小时的时间。记录下来,加深理解。