Redis (六) Lua 脚本

Lua 脚本:

  Lua/ˈluə/是一种轻量级脚本语言,它是用 C 语言编写的,跟数据的存储过程有点类似。 使用 Lua 脚本来执行 Redis 命令的好处:

  1. 一次发送多个命令,减少网络开销。
  2. Redis 会将整个脚本作为一个整体执行,不会被其他请求打断,保持原子性。
  3. 对于复杂的组合命令,我们可以放在文件中,可以实现程序之间的命令集复用。

在Redis Cli 中 调用 Lua 脚本:

  使用 eval /ɪ'væl/ 方法,语法格式:

127.0.0.1:6379> eval lua-script key-num [key1 key2 key3 ....] [value1 value2 value3 ....]
eval 代表执行 Lua 语言的命令。 lua-script 代表 Lua 语言脚本内容。 key-num 表示参数中有多少个 key,需要注意的是 Redis 中 key 是从 1 开始的,如果没有 key 的参数,那么写 0。 [key1 key2 key3…]是 key 作为参数传递给 Lua 语言,也可以不填,但是需要和 key-num 的个数对应起来。 [value1 value2 value3 ….]这些参数传递给 Lua 语言,它们是可填可不填的。

  例如:

在 Lua 脚本 中调用 Redis 命令:

  使用 redis.call(command, key [param1, param2…])进行操作。语法格式:

redis> eval "redis.call('set',KEYS[1],ARGV[1])" 1 lua-key lua-value
command 是命令,包括 set、get、del 等。
key 是被操作的键。
param1,param2…代表给 key 的参数。

  注意跟 Java 不一样,定义只有形参,调用只有实参。Lua 是在调用时用 key 表示形参,argv 表示参数值(实参)。在 Redis 中调用 Lua 脚本执行 Redis 命令

  以上命令等价于 set name wuzz。

在 Redis 中调用 Lua 脚本 文件中的命令 ,

  操作 Redis创建 Lua 脚本文件:

//创建文件
cd /mysoft/redis-4.0.8/src
vim wuzz.lua
//Lua 脚本内容,先设置,再取值:
redis.call('set','wuzz',hello)
return redis.call('get',wuzz)
//在 Redis 客户端中调用 Lua 脚本 0 位参数个数
cd /mysoft/redis-4.0.8/src
redis-cli --eval wuzz.lua 0

  案例 :对 IP 进行限流

  需求:在 X 秒内只能访问 Y 次。

  设计思路:用 key 记录 IP,用 value 记录访问次数。拿到 IP 以后,对 IP+1。如果是第一次访问,对 key 设置过期时间(参数 1)。否则判断次数,超过限定的次数(参数 2),返回 0。如果没有超过次数则返回 1。超过时间,key 过期之后,可以再次访问。KEY[1]是 IP, ARGV[1]是过期时间 X,ARGV[2]是限制访问的次数 Y。

-- ip_limit.lua
-- IP 限流,对某个 IP 频率进行限制 ,5 秒钟访问 2 次
local num=redis.call('incr',KEYS[1])
if tonumber(num)==1 then
    redis.call('expire',KEYS[1],ARGV[1])
    return 1
elseif tonumber(num)>tonumber(ARGV[2]) then
    return 0
else
    return 1
end

  5 秒钟内限制访问 2 次,调用测试(连续调用 2 次):

./redis-cli  -h 127.0.0.1 -p 6379 -a wuzhenzhao   --eval "ip_limit.lua" app:ip:limit:192.168.8.111 , 5 2

Jedis 调用 lua 脚本相关操作:

public static void main(String[] args) {
        Jedis jedis = getJedisUtil();
        for(int i=0; i<5; i++){
            limit();
        }
}

/**
* 5秒内限制访问2次
*/
public static void limit(){
        Jedis jedis = getJedisUtil();
        // 只在第一次对key设置过期时间
        String lua = "local num = redis.call('incr', KEYS[1])\n" +
                "if tonumber(num) == 1 then\n" +
                "\tredis.call('expire', KEYS[1], ARGV[1])\n" +
                "\treturn 1\n" +
                "elseif tonumber(num) > tonumber(ARGV[2]) then\n" +
                "\treturn 0\n" +
                "else \n" +
                "\treturn 1\n" +
                "end\n";
        Object result = jedis.evalsha(jedis.scriptLoad(lua), Arrays.asList("localhost"), Arrays.asList("5", "2"));
        System.out.println(result);
}

private static Jedis getJedisUtil() {
        String ip = ResourceUtil.getKey("redis.host");
        int port = Integer.valueOf(ResourceUtil.getKey("redis.port"));
        String password = ResourceUtil.getKey("redis.password");
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        JedisPool pool = new JedisPool(jedisPoolConfig, ip, port, 10000, password);
        return pool.getResource();
}

缓存 Lua 脚本:

  为什么要缓存?在脚本比较长的情况下,如果每次调用脚本都需要把整个脚本传给 Redis 服务端,会产生比较大的网络开销。为了解决这个问题,Redis 提供了 EVALSHA 命令,允许开发者通过脚本内容的 SHA1 摘要来执行脚本。

  Redis 在执行 script load 命令时会计算脚本的 SHA1 摘要并记录在脚本缓存中,执行 EVALSHA 命令时 Redis 会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果找到了则执行脚本,否则会返回错误:"NOSCRIPT No matching script. Please useEVAL."

  脚本超时:

  Redis 的指令执行本身是单线程的,这个线程还要执行客户端的 Lua 脚本,如果 Lua脚本执行超时或者陷入了死循环,是不是没有办法为客户端提供服务了呢?

  使用  eval 'while(true) do end' 0 来模拟死循环,为了防止某个脚本执行时间过长导致 Redis 无法提供服务,Redis 提供了 lua-time-limit 参数限制脚本的最长运行时间,默认为 5 秒钟。

  lua-time-limit 5000(redis.conf 配置文件中) 当脚本运行时间超过这一限制后,Redis 将开始接受其他命令但不会执行(以确保脚本的原子性,因为此时脚本并没有被终止),而是会返回“BUSY”错误。Redis 提供了一个 script kill 的命令来中止脚本的执行。新开一个客户端:script kill

  如果当前执行的 Lua 脚本对 Redis 的数据进行了修改(SET、DEL 等),那么通过 script kill 命令是不能终止脚本运行的。因为要保证脚本运行的原子性,如果脚本执行了一部分终止,那就违背了脚本原子性的要求。最终要保证脚本要么都执行,要么都不执行。遇到这种情况,只能通过 shutdown nosave 命令来强行终止 redis。shutdown nosave 和 shutdown 的区别在于 shutdown nosave 不会进行持久化操作,意味着发生在上一次快照后的数据库修改都会丢失。

  如果我们有一些特殊的需求,可以用 Lua 来实现。

posted @ 2020-10-09 02:55  跃小云  阅读(583)  评论(0编辑  收藏  举报