Redis EVAL执行Lua脚本之批量删除数据

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lupengfei1009/article/details/86619160

前言

redis数据库目前已经成为项目中不可或缺的一部分,在项目开发中出镜率非常的高;Lua是一个小巧的脚本语言,灵活性很强;从redis2.6.0版本之后,内置了Lua的解析器,可以通过redis执行lua脚本;

插个题外话,nginx+redis+lua可搭建高并发方案,想了解的朋友可以通过以下连接查看:
OpenResty(Nginx+Lua)高并发最佳实践
OpenResty高并发最佳实践–Redis操作
OpenResty高并发最佳实践–mysql操作

EVAL介绍

  • 指令格式
    EVAL script numkeys key [key …] arg [arg …]
    script:待执行的脚本
    numkeys:key的个数
    [key …]:对应的key,可以是一个,可以是多个
    [arg …]:与key对应的值,可以是一个,可以是多个
  • Lua获取传参数据
    记住,Lua的下表索引是从1开始的
    • key的获取方式
      KEYS[下标索引],如KEYS[1],取第一个值
    • 值的获取
      ARGV[1]
  • 示例
    eval “return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}” 2 key1 key2 first second

批量删除

  • 使用场景
    假如说现在redis中一批数据,key的格式很规范,都是以test_id的形式存储在redis里面,由于某一天,这个业务下架了,需要把这部分数据给删除掉,清理出这部分废数据,我们可以怎么做呢?
    • 第一,通过终端删除
      比如,使用java,或者shell通过redis的keys指令,获取到数据的key,然后再使用for循环删除;shell脚本操作0库很容易,但是如果要存在切库的话,就会很麻烦。
    • 第二,使用Lua脚本(推荐使用)
      通过写一段Lua脚本,脚本的操作方式的业务逻辑一样,也是先keys,然后循环删除
  • 为什么要使用Lua呢
    上面说了两种方式,操作的方式和原理都是一样的,为什么要推荐使用Lua这种方式呢,有以下几个原因
    • 原子性
      Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。这和使用 MULTI / EXEC 包围的事务很类似。在其他别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)。
      另一方面,这也意味着,执行一个运行缓慢的脚本并不是一个好主意。写一个跑得很快很顺溜的脚本并不难,因为脚本的运行开销(overhead)非常少,但是当你不得不使用一些跑得比较慢的脚本时,请小心,因为当这些蜗牛脚本在慢吞吞地运行的时候,其他客户端会因为服务器正忙而无法执行命令。
    • 减少带宽吞吐
      EVAL 命令要求你在每次执行脚本的时候都发送一次脚本主体(script body)。Redis 有一个内部的缓存机制,因此它不会每次都重新编译脚本,不过在很多场合,付出无谓的带宽来传送脚本主体并不是最佳选择。
    • 缓存及复用
      Redis 保证所有被运行过的脚本都会被永久保存在脚本缓存当中,这意味着,当 EVAL 命令在一个 Redis 实例上成功执行某个脚本之后,随后针对这个脚本的所有 EVALSHA 命令都会成功执行。
    • 可操作性
      使用Lua写一个可执行的脚本很快就可以完成,而使用java或其他终端,可能需要花很久。
使用keys获取数据删除
  • 脚本编写
    在linux上的某个文件夹下创建通用删除lua脚本:clear_data.lua,并将以下代码拷贝进去:
    --选库
    redis.call('select',7)
    --获取传入的需要批量删除的key的前缀
    --记住 lua的下标索引是从1开始 不是0 不是0 不是0
    local key = KEYS[1]
    
    --如果没有传至 跳过
    if( key ~= nil) then
            --这里通过keys查询出所有符合条件的数据
            local dataInfos = redis.call('keys',KEYS[1])
            --判断是否找到数据
            if(dataInfos ~= nil) then
                    --循环删除
                    for i=1,#dataInfos,1 do
                            redis.call('del',dataInfos[i])
                    end
                    --返回删除的行数
                    return #dataInfos
            else
                    return 0
            end
    else
            return 0
    end
    
使用scan获取数据删除(推荐使用)
  • 为什么推荐使用scan
    我们知道redis是一个单线程的,当我们库里面存在大量数据的时候,使用keys *的方式匹配数据的时候,可能需要好几秒才能处理完,那么在这个几秒的时间里是处于线程阻塞的,其他所有的redis操作都是处于等待状态,这样对系统的可用性是有影响的,因此,这里使用scan的方式匹配数据。
  • scan介绍
    SCAN cursor [MATCH pattern] [COUNT count]
    SCAN 命令是一个基于游标的迭代器(cursor based iterator): SCAN 命令每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。
    当 SCAN 命令的游标参数被设置为 0 时, 服务器将开始一次新的迭代, 而当服务器向用户返回值为 0 的游标时, 表示迭代已结束。
    通俗点理解就是,基于游标的迭代器redis会慢慢一次次的将数据返回回来,从而防止线程阻塞。使用示例如下:
    127.0.0.1:6379[7]> scan 0 match test_*
    1) "1"
    2)  1) "test_1"
        2) "test_11"
        3) "test_4"
        4) "test_2"
        5) "test_7"
        6) "test_5"
        7) "test_9"
        8) "test_3"
        9) "test_8"
       10) "test_10"
    127.0.0.1:6379[7]> scan 1 match test_*
    1) "0"
    2) 1) "test_6"
    
    更多详情操作可参考scan api
  • 脚本编写
    在linux上的某个文件夹下创建通用删除lua脚本:clear_data.lua,并将以下代码拷贝进去:
    redis.call("select",7)
    
    --游标的id
    local cursor = 0
    --查找删除的key的数量
    local keyNum = 0
    repeat
      --使用scan搜索,cursor=0的时候标识一个新的迭代期,服务器返回0的时候表示迭代已经结束
      local res = redis.call("scan",cursor,"MATCH",KEYS[1])
      if(res ~= nil and #res>=0) then
        cursor = tonumber(res[1])
        local ks = res[2]
        if(ks ~= nil and #ks>0) then
          redis.replicate_commands()
          --循环删除当前迭代器迭代出的数据
          for i=1,#ks,1 do
            local key = tostring(ks[i])
            --使用UNLINK删除,区别于del的是这个是异步执行的
            --这条指令要版本大于4.0.0 小于4.0.0就使用del
            redis.call("UNLINK",key)
          end
          --统计删除的key的数量
          keyNum = keyNum + #ks
        end
      end
    --当服务器返回0的时候,跳出循环
    until( cursor <= 0 )
    
    return keyNum
    
    UNLINK操作请参考官网文档说明

脚本执行

  • 通过redis-cli执行脚本
    指令格式如下

    redis-cli -h 地址 -p 端口 -a 密码 --eval 以上创建文件的路径/clear_data.lua "需要删除的key的前缀_*"
    
  • 多值传送示例

    redis-cli -h 地址 -p 端口 -a 密码 --eval 以上创建文件的路径/clear_data.lua "key1" "key2" , "va1" "va2"
    
  • 脚本调用示例

    redis-cli -h 127.0.0.1 -p 6379 -a 123456789 --eval /usr/local/redis/clear_data.lua "test_*"
    

    执行以上指令,就会将redis下以DATA_ID_开头的key全部删掉,如下图:

linux定时任务通过shell执行redis脚本

  • 脚本示例
    redis_test_data.txt
    select 7
    set test_1 1
    set test_2 2
    set test_3 3
    set test_4 4
    set test_5 5
    set test_6 6
    set test_7 7
    set test_8 8
    set test_9 9
    set test_10 10
    set test_11 11
    
  • linux执行脚本
    cat 脚本路径/redis_test_data.txt | redis-cli -h 地址 -p 端口 -a 密码
    
    示例:
    cat /usr/local/redis/data/redis_test_data.txt | redis-cli -h 127.0.0.1 -p 6379 -a 123456789
    

  • linux定时任务执行redis脚本失败的问题
    实际生成环境中会出现以上脚本直接手动执行是可以正常的,但是使用定时任务执行就失败了,经过分析测试,发现是定时任务执行的时候,环境变量没有生效,导致指令执行失败,因此需要对以上指令进行优化
    cat 脚本路径/redis_info.txt | redis安装路径/src/redis-cli(全路径) -h 地址 -p 端口 -a 密码
    示例:
    cat /usr/local/redis/data/redis_info.txt | /usr/local/redis/src/redis-cli -h 127.0.0.1 -p 6379 -a 123456789
    

总结

通过redis执行Lua脚本这个操作,可以让我们很多基础的运维操作变的很简单,开发过程中,也可以通过这种执行脚本的方式来降低redis的操作频率,将redis的性能发挥到极致…

猜你喜欢

转载自blog.csdn.net/lupengfei1009/article/details/86619160