作者:铂赛东 \
链接:www.jianshu.com/p/8cf8aac3dc25
1
本来认为本人对 redis 命令还蛮相熟的,各种数据模型各种基于 redis 的骚操作。然而最近在应用 redis 的 scan 的命令式却踩了一个坑,登时察觉本人原来对 redis 的游标了解的很无限。所以记录下这个踩坑的过程,背景如下:
公司因为 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。
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
在本地的测试 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 个多小时的工夫。记录下来,加深了解。
另外,关注公众号 Java 技术栈,在后盾回复:面试,能够获取我整顿的 Redis 系列面试题和答案,十分齐全。
近期热文举荐:
1.600+ 道 Java 面试题及答案整顿(2021 最新版)
2. 终于靠开源我的项目弄到 IntelliJ IDEA 激活码了,真香!
3. 阿里 Mock 工具正式开源,干掉市面上所有 Mock 工具!
4.Spring Cloud 2020.0.0 正式公布,全新颠覆性版本!
5.《Java 开发手册(嵩山版)》最新公布,速速下载!
感觉不错,别忘了顺手点赞 + 转发哦!