1 在redis中调用lua脚本 使用eval方法,语法格式:
1 2 3 4 5 6 7 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 ...]参数的value值,一一对应,可填可不填
示例:返回一个字符串,0个参数:
1 eval "return 'Hello World'" 0
实际上,Lua脚本在Redis里面真正的用途是用来执行redis命令。
2 在Lua脚本中调用redis命令 2.1 命令格式 使用redis.call(command, key, [param1, param2 ……])进行操作。语法格式:
1 2 3 4 redis.call(command, key [param1, param2, ...])
一个简单的案例,让Lua脚本执行set qingshan 2673
1 2 eval "return redis.call('set','qingshan','2673')" // 写死值 eval "return redis.call('set', KEYS[1], ARGV[1])" 1 qingshan 2673 // 参数传递
在redis-cli中直接写Lua脚本不够方便,也不能实现编辑和复用,通常我们会把lua脚本凡在文件中,然后执行这个文件。
2.2 Lua脚本文件 创建脚本文件
1 2 cd /usr/local/soft/redis-6.0.9/src vim test.lua
Lua脚本内容,先赋值,再取值:
1 2 redis.call('set' ,'qingshan' ,'lua666' )return redis.call('get' ,'qingshan' )
调用脚本文件
1 2 cd /usr/local/soft/redis-6.0.9/src redis-cli --eval test.lua 0
2.3 案例:对IP进行限流 需求:每个用户再X秒内只能访问Y次。设计思路:
首先是数据类型。用String的key记录IP,用value记录访问的次数。几秒钟和几次哟啊用参数动态传进去。拿到IP以后,对IP+1。如果是第一次访问,对key设置国企时间(参数1).否则判断次数,超过限定次数(参数2),返回0。如果没有超过次数返回1。超过时间,key国企之后,可以再次访问。
KEY[1]是IP,ARGV[1]是过期时间X,ARGV[2]是限制访问的次数Y。
1 2 3 4 5 6 7 8 9 10 11 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
6秒钟内限制访问10次,调用测试(连续调用10次)
1 redis-cli --eval ip_limit.lua app:ip:limit :192.168.8.111 , 6 10
Java代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 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(); } } public static void limit () { Jedis jedis = getJedisUtil(); String lua = "local num = redis.call('incr', KEYS[1]) \n" + "if tonumber(num) == 1 then\n" + "\t redis.call('expire', KEYS[1], ARGV[1]) \n" + "\t return 1 \n" + "elseif tonumber(num) > tonumber(ARGV[2]) then \n" + "\t return 0 \n" + "else \n" + "\t return 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(); } }
2.4 缓存Lua脚本 为什么要缓存
在Lua脚本比较长的情况下,如果多次调用脚本都需要把整个脚本传给Redis服务端,会产生比较大的网络开销。为了解决这个问题,Redis可以缓存Lua脚本并生成SHA1摘要码,后面可以直接通过摘要码来执行Lua脚本。
如何缓存
这里面涉及到两个命令,首先是在服务端缓存lua脚本生成一个摘要码,用script load命令
1 script load "return Hello World"
第二个命令是通过摘要码执行缓存的脚本:
自乘案例
Redis有incrby这样的自增命令,但是没有自乘。比如乘以3,乘以5
写一个自乘的运算,让它乘以后面的参数:
1 2 3 4 5 6 7 8 9 local curVal=redis.call("get" ,KEYS[1 ])if curVal==false then curVal=0 else curVal=tonumber (curVal)end curVal=curVal*tonumber (ARGV[1 ]) redis.call("set" ,KEYS[1 ],curVal)return curVal
这个命令变成串行,语句之间使用分号隔开,Script load命令(redis客户端执行)
1 script locad 'local curVal=redis.call("get",KEYS[1]);if curVal==false then curVal=0 else curVal=tonumber(curVal) end;curVal=curVal*tonumber(ARGV[1]);redis.call("set",KEYS[1],curVal);return curVal'
调用
2.5 脚本超时 Redis的指令执行本身是单线程的,这个线程还要执行客户端的Lua脚本,如果Lua脚本执行超时或者陷入了死循环,是不是没有办法为客户端提供服务了?它会导致其他的命令都会进入等待状态。为了防止这种情况,首先,脚本执行有一个超时时间,默认为5秒钟。
超过5秒钟,其他客户端的命令不会等待,而是直接返回BUSY错误。这样也不行,不能一直拒绝其他客户端的命令执行。在提示中看到有两个命令可以使用,第一个是script kill,终止脚本的执行。但是需要注意:并不是所有的lua脚本执行都可以kill。如果当前执行的lua脚本对Redis的数据进行了修改(set,DEL、等),那么通过script kill命令是不能终止脚本运行的。为什么包含修改的脚本不能中断?因为要保证脚本运行的原子性。如果脚本执行了一部分被终止,那就违背了脚本原子性的目标。遇到这种情况,只能通过shutdown nosave命令,直接把redis服务停掉。正常关机是shutdown。Shutdown nosave和shutdown的区别在于shutdown nosave不会进行持久化操作,意味着发生在上一个快照后的数据库修改都会丢失。