Redis中Lua脚本相关命令及注意事项

Redis中Lua脚本相关命令及注意事项

两篇介绍了Lua的常用例子后,相信已经对Lua脚本的大概作用,以及能做的事情有了了解,本篇就大概介绍一下应该怎么写一个脚本。我对Lua是有感情的,主要是对魔兽世界有感情,而魔兽世界的游戏插件就是用Lua写的,写Lua时有时甚至会回忆起以前,以前为团队写自己的公会的插件,因为一些BUG,让公会的哥们们灭了一周,最后发现是我插件提示错了。想想很让人怀念。前几年一度很火的《愤怒的小鸟》的关卡也是。还有高性能Web平台OpenResty(Nginx+Lua)也用到了Lua。再加上Lua的语法简单,很容易学习。而Redis中的Lua脚本,大部分只需要Lua的基本语法就能满足很多操作。

1.EVAL命令

Redis提供了EVAL命令可以使我们像调用内置命令一样调用脚本。
命令格式:脚本内容,key参数的个数,[key. ..][arg …].可用通过key和arg两类参数各脚本传递参数,它们的值可以在脚本中分别使用KEYS和ARGV两个表类型的全局变量访问。其中要读写的键名应作为key参数,其它的数据都要作为arg参数。虽然事实上这一要求并不是强制的,如:EAVL "return redis.call('get',KEYS[1])" 1 aaa 可以获取aaa的键值,同样还可以这么写EAVL "return redis.call('get','a' .. ARGV[1])" 0 aa,结果是一样的。反转来了。但是不遵守规则会付出代价的。在Redis集群时,集群的作用就是将键分散至不同的节点上。这就意味着在脚本执行前就需要知道脚本要操作的键对应的节点,Redis会根据KEYS中的参数,找到对应节点,如果你的KEYS里值不是脚本操作的健,不好意思,该脚本是无法兼容集群的就如我们上一篇中看到的Lua在Redis中的应用-分页缓存 说的那样

 private List<Object> getDataByPageForLua(Jedis redis, String key, int pageSize, int pageNum) {
        String  script = " local list = redis.call('zrange',KEYS[1],ARGV[1],ARGV[2]) "
                + " local result = {} "
                + " for k,v in pairs(list) do"
                + "  local r = redis.call('get',KEYS[1] .. '.' .. v) "
                + "  if(r) then table.insert(result,r) eles redis.call('zrem',KEYS[1],v) end"
                + " end return result ";
        List<String> result = (List<String>) redis.eval(script, 1,key,pageSize*pageNum+"",(pageSize*pageNum+pageSize)+"");
        if(result!=null) return result.stream().map(n->{
            return JSON.parseObject(n, Object.class);
        }).collect(Collectors.toList());
        else return null;
    }

local r = redis.call('get',KEYS[1] .. '.' .. v)里我们操作了不在KEYS里的键,就会报该KEY不在该NODE上的错误。要避免这个问题,要么强制使用HashTag使脚本中要操作的与KEYS里的参数强制保存至同一个集群节点上如:KEYS的参数为:{lhn} loca r = redis.call('get',KEYS[1] .. '.' .. v)就必须操作的键为:{lhn}XXXXX,而这些健要在一开始就保存成这种形式,所以从保存时就要规划好健的形式,而且这么做也不太好,会使保存的健在集群上分布不均匀。SO更好的办法,最好是将上面的 第2行单独获取 redis.call('zrange',KEYS[1],ARGV[1],ARGV[2]) 获取到ID列然后使用pipelining来实现了。
总之:
1、脚本中的所有键必须在 cluster 中的同一个节点中。要想让 script 能在 集群 下正常工作,必须要把会用到的键名明确指出。这样节点在收到 eval 命令后就能分析出所要操作的键是不是都在一个节点里了,如果是则正常处理,不是就返回 CROSSSLOT 错误。如果不明确指出,在脚本中就会报错。
2、要么Hash tag上同一个节点。

EVALSH

如果脚本比较长,每次调用脚本都需要将整个脚本传给Redis会占用较多带宽。为了解决这个问题,Redis提供了EVALSHA命令,允许我们通过脚本内容的SHA1摘要来执行脚本,命令的用法与EVAL一样,只不过是将脚本的内容替换成SHA1摘要。

SCRIPT LOAD

将脚本加入脚本缓存而不执行,返回值为脚本的SHA1摘要。

SCRIPT EXISTS

同时查找1个或者多个脚本的SHA1摘要是否被缓存

SCRIPT FLUSH

脚本的SHA1摘要加入到脚本缓存后会永久保留,不会删除,但可以手动使用该命令清空脚本缓存

SCRIPT KILL

终止当前正在执行的脚本。

注意

Redis脚本是原子的,也就是执行期间Redis不会执行其他命令。所有的命令都必须等待脚本完成才能执行。为了防止脚本执行时间过长导致Redis无法提供服务。Redis提供了lua-time-limit参数,限制脚本执行时间,默认为5秒。当脚本运行时间超过这一限制后,脚本开始接受其它命令,但不会执行(保证脚本原子性,此时脚本还没有被终止),而是会返回异常错误。

如下
1、在一客户端中先执行eval "while true do end" 0
2、另一个客户端执行 get aaa 这时get aaa 并没有马上返回结果,5秒后返回错误
此时Redis虽然可以接受命令,但无法执行,这种情况下Redis只能执行两个命令 就是上面说的:SCRIPT KILLSHUTDOWN NOSAVE
如果当前执行脚本中对Redis进行了修改(如:SET、LPUSH、DEL等)则SCRIPT KILL是无法终止脚本运行,以防止脚本只执行了一部分。因为会违背脚本的原子性原则。
如:EAVL "redis.call('set','aaa','lhn') while turn do end" 0 此时SCRIPT KILL 是无法终止脚本的。此时只能执行SHUTDOWN NOSAVE强行终止Redis, 这是很危险的 ,所以在写脚本时,要很小的避免这种使脚本无法终止的错误,一定要测试

总结

Redis的脚本是非常高效的,绝大部分情况下是不用考虑脚本的性能问题的。脚本的强大功能,使很多原本在程序中执行的逻辑都可以放在脚本中,但也要明白不应该把进行大量耗时的计算或者复杂的逻辑写到脚本中,毕竟Redis中的脚本调试不如程序那么好,而且Redis是单进程单线程执行脚本,而程序是可以多实例多进程多线程运行。

猜你喜欢

转载自blog.csdn.net/lhn1234321/article/details/79029513