Redis:使用scan配合lua脚本进行删除key

Redis SCAN 命令

Redis key(键)](https://www.runoob.com/redis/redis-keys.html "Redis key(键)")

Redis Scan 命令用于迭代数据库中的数据库键。

SCAN 命令是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。

SCAN 返回一个包含两个元素的数组, 第一个元素是用于进行下一次迭代的新游标, 而第二个元素则是一个数组, 这个数组中包含了所有被迭代的元素。如果新游标返回 0 表示迭代已结束。

相关命令:

  • SSCAN 命令用于迭代集合键中的元素。
  • HSCAN 命令用于迭代哈希键中的键值对。
  • ZSCAN 命令用于迭代有序集合中的元素(包括元素成员和元素分值)。

语法

redis Scan 命令基本语法如下:

  • cursor - 游标。
  • pattern - 匹配的模式。
  • count - 指定从数据集里返回多少元素,默认值为 10 。

可用版本

= 2.8.0

返回值

数组列表。

scan和keys的区别

redis的keys命令,通来在用来删除相关的key时使用,但这个命令有一个弊端,在redis拥有数百万及以上的keys的时候,会执行的比较慢,更为致命的是,这个命令会阻塞redis多路复用的io主线程,如果这个线程阻塞,在此执行之间其他的发送向redis服务端的命令,都会阻塞,从而引发一系列级联反应,导致瞬间响应卡顿,从而引发超时等问题,所以应该在生产环境禁止用使用keys和类似的命令smembers,这种时间复杂度为O(N),且会阻塞主线程的命令,是非常危险的。

keys命令的原理就是扫描整个redis里面所有的db的key数据,然后根据我们的通配的字符串进行模糊查找出来。官网详细的介绍如下。

https://redis.io/commands/KEYS
取而代之的,如果需要查找然后删除key的需求,那么在生产环境我们应该使用scan命令,代替keys命令,同样是O(N)复杂度的scan命令,支持通配查找,scan命令或者其他的scan如SSCAN ,HSCAN,ZSCAN命令,可以不用阻塞主线程,并支持游标按批次迭代返回数据,所以是比较理想的选择。keys相比scan命令优点是,keys是一次返回,而scan是需要迭代多次返回。

https://redis.io/commands/scan
但scan命令的也有缺点,返回的数据有可能重复,需要我们在业务层按需要去重,scan命令的游标从0开始,也从0结束,每次返回的数据,都会返回下一次游标应该传的值,我们根据这个值,再去进行下一次的访问,如果返回的数据为空,并不代表没有数据了,只有游标返回的值是0的情况下代表结束。

关于key是删除

1、清楚某个服务或者某种资源的key,往往是用模糊匹配去清楚,使用场景:某个微服务中某写缓存发生了database级别的变化,在不停redis的情况下,如何高效的清除这些缓存。

2、redis大key,这里指的是大的集合数据类型,如(set/hash/list/sorted set),一个key包含很多元素。由于redis是单线程,在删除大key(千万级别的set集合)的时候,或者清理过期大key数据时,主线程忙于删除这个大key,会导致redis阻塞、崩溃,应用程序异常的情况。

但在删除key的时候,要避免keys命令的使用,这个常识已深入人心。

RedisTemplate 已经封装了关于 set/hash/sorted set 的scan

       redisTemplate.opsForHash().scan();
        redisTemplate.opsForSet().scan();
        redisTemplate.opsForZSet().scan();

下面介绍使用scan配合lua脚本进行删除。

lua 脚本
local function scan(key)
    local cursor = 0
    local keynum = 0

    repeat
        local res = redis.call("scan", cursor, "match", key,'COUNT',ARGV[1])

        if (res ~= nil and #res >= 0) then
            redis.replicate_commands()
            cursor = tonumber(res[1])
            local ks = res[2]
            keynum = #ks
            for i=1,keynum,1 do
                local k = tostring(ks[i])
                redis.call("del", k)
            end
        end
    until (cursor <= 0)

    return keynum
end

local a = #KEYS
local b = 1
local total = 0
while (b <= a)
do
    total = total + scan(KEYS[b])
    b = b + 1
end

return total

1、key [key ...]: 表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 1 为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。

2、arg [arg ...]: 附加参数,在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。

springboot 中使用RedisTemplate执行脚本,测试通过
String del = "local function scan(key)\n" +
                "    local cursor = 0\n" +
                "    local keynum = 0\n" +
                "\n" +
                "    repeat\n" +
                "        local res = redis.call(\"scan\", cursor, \"match\", key,'COUNT',ARGV[1])\n" +
                "\n" +
                "        if (res ~= nil and #res >= 0) then\n" +
                "            redis.replicate_commands()\n" +
                "            cursor = tonumber(res[1])\n" +
                "            local ks = res[2]\n" +
                "            keynum = #ks\n" +
                "            for i=1,keynum,1 do\n" +
                "                local k = tostring(ks[i])\n" +
                "                redis.call(\"del\", k)\n" +
                "            end\n" +
                "        end\n" +
                "    until (cursor <= 0)\n" +
                "\n" +
                "    return keynum\n" +
                "end\n" +
                "\n" +
                "local a = #KEYS\n" +
                "local b = 1\n" +
                "local total = 0\n" +
                "while (b <= a)\n" +
                "do\n" +
                "    total = total + scan(KEYS[b])\n" +
                "    b = b + 1\n" +
                "end\n" +
                "\n" +
                "return total";
        System.out.println(del);
        //KEY[1]  为要删除的前缀  举例为 Test.* 代表以Test.开头的所有Key
        //ARGV[1] 单次遍历的数量 注意 不是返回的数量,举例 服务器总共Key有10万  设置为20000则需要循环5次才能全部遍历完

        // 指定 lua 脚本,并且指定返回值类型
        DefaultRedisScript redisScript = new DefaultRedisScript<>(del, Long.class);
        // 参数一:redisScript,参数二:key列表,参数三:arg(可多个)
        Long result = (Long) redisTemplate.execute(redisScript, Collections.singletonList("myServiceCache*"),1000);
        System.out.println(result);
redis 中执行 使用eval命令执行lua脚本
1、redis-cli命令 连接redis
2、使用eval命令

127.0.0.1:6379> eval "local function scan(key)    local cursor = 0    local keynum = 0    repeat        local res = redis.call("scan", cursor, "match", key,'COUNT',ARGV[1])        if (res ~= nil and #res >= 0) then            redis.replicate_commands()            cursor = tonumber(res[1])            local ks = res[2]            keynum = #ks            for i=1,keynum,1 do                local k = tostring(ks[i])                redis.call("del", k)            end        end    until (cursor <= 0)    return keynum end  local a = #KEYS local b = 1 local total = 0 while (b <= a) do    total = total + scan(KEYS[b])    b = b + 1 end return total" 1 loadU* , 1000
Invalid argument(s)
127.0.0.1:6379> 

3、但是出现报错,带解决。有了解的大神,还望不吝赐教。

你可能感兴趣的:(Redis:使用scan配合lua脚本进行删除key)