01、使用Lua脚本来执行Redis命令的好处

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

02、Redis中调用Lua脚本

使用eval方法,语法格式如下:

redis lua脚本 set操作 redis lua脚本语法_redis

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

示例,返回一个字符串,0个参数:

redis lua脚本 set操作 redis lua脚本语法_redis_02

03、在Lua脚本中调用Redis命令

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

redis lua脚本 set操作 redis lua脚本语法_redis_03

  • command是命令,包括set、get、del等。
  • key是被操作的键。
  • param1,param2...代表给key的参数。 

【1】、在Redis中调用Lua脚本执行Redis命令

redis lua脚本 set操作 redis lua脚本语法_Lua_04

以上命令等价于setgupao2673。

【2】、Redis中调用Lua脚本文件中的命令,操作Redis

创建Lua脚本文件:

redis lua脚本 set操作 redis lua脚本语法_redis lua脚本 set操作_05

Lua脚本内容,先设置,再取值:

redis lua脚本 set操作 redis lua脚本语法_redis_06

在Redis客户端中调用Lua脚本 

redis lua脚本 set操作 redis lua脚本语法_redis_07

得到返回值:

redis lua脚本 set操作 redis lua脚本语法_redis lua脚本 set操作_08

【3】、案例 对IP进行限流

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

设计思路:用key记录IP,用value记录访问次数。

拿到IP以后,对IP+1。如果是第一次访问,对key设置过期时间(参数1)。否则判断次数,超过限定的次数(参数2),返回0。如果没有超过次数则返回1。超过时间,key过期之后,可以再次访问。

KEY[1]是IP,ARGV[1]是过期时间X,ARGV[2]是限制访问的次数Y。

redis lua脚本 set操作 redis lua脚本语法_redis_09

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

redis lua脚本 set操作 redis lua脚本语法_Redis_10

app:ip:limit:192.168.8.111是key值,后面是参数值,中间要加上一个空格和一个逗号,再加上一个空格。

即:./redis-cli–eval [lua脚本] [key...] 空格,空格[args...]

多个参数之间用一个空格分割。

03、缓存Lua脚本

【1】、为什么要缓存

在脚本比较长的情况下,如果每次调用脚本都需要把整个脚本传给Redis服务端,会产生比较大的网络开销。

【2】、如何缓存

Redis提供了EVALSHA命令,允许开发者通过脚本内容的SHA1摘要来执行脚本。

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

redis lua脚本 set操作 redis lua脚本语法_Redis_11

【3】、案例 自乘案例

自乘的运算,让它乘以后面的参数:

redis lua脚本 set操作 redis lua脚本语法_redis_12

把这个脚本变成单行,语句之间使用分号隔开

redis lua脚本 set操作 redis lua脚本语法_Lua_13

 script load  '命令'

redis lua脚本 set操作 redis lua脚本语法_Redis_14

 返回

"be4f93d8a5379e5e5b768a74e77c8a4eb0434441"

调用:

127.0.0.1:6379>set num 2
OK
127.0.0.1:6379>evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 num 6
 (integer)12

04、脚本超时

为了防止某个脚本执行时间过长导致Redis无法提供服务,Redis提供了lua-time-limit参数限制脚本的最长运行时间,默认为5秒钟。

lua-time-limit 5000(redis.conf配置文件中)

Redis提供了一个script kill的命令来中止脚本的执行。新开一个客户端:

 

redis lua脚本 set操作 redis lua脚本语法_Redis_15

 如果当前执行的Lua脚本对Redis的数据进行了修改(SET、DEL等),那么通过scriptkill命令是不能终止脚本运行的。

redis lua脚本 set操作 redis lua脚本语法_redis_16

因为要保证脚本运行的原子性,如果脚本执行了一部分终止,那就违背了脚本原子性的要求。最终要保证脚本要么都执行,要么都不执行。

127.0.0.1:6379>script kill
(error)UNKILLABLE Sorry the script already executed write commands against the dataset.You can either wait the script terminationor kill the server in a hard way using the SHUTDOWN NOSAVE command.

遇到这种情况,只能通过shutdown nosave命令来强行终止redis。 

05、java调用案例

public class LuaTest {
     public static void main(String[] args) {
         Jedis jedis = getJedisUtil();
         jedis.eval("return redis.call('set',KEYS[1],ARGV[1])", 1,"test:lua:key","qingshan2673lua");
         System.out.println(jedis.get("test:lua:key"));
         for(int i=0; i<10; i++){
             limit();
         }
     }     /**
      * 10秒内限制访问5次
      */
     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("10", "5"));
         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();
     }}