Redis与Lua
核心知识点:
1.Redis中执行Lua脚本的两种方法:
a.eval
b.evalsha
2.Lua中对redis的访问(redis.call())
3.管理Lua脚本
a.script load:加载
b.script exists:判断
c.script flush:清除
d.script kill:杀死(如果已经执行过写操作,必须使用shutdown nosave)重启服务
1.在Redis中使用Lua
在Redis中执行Lua脚本有两种方法:eval和evalsha。
(1)eval
语法:
eval 脚本内容 key个数 key列表 参数列表
下面例子使用了key列表和参数列表来为Lua脚本提供更多的灵活性:
127.0.0.1:6379> eval 'return "hello" .. KEYS[1]..ARGV[1]' 1 redis world "helloredisworld"
此时KEYS[1]="redis",ARGV[1]="world",所以最终的返回结果是“helloredisworld”。
如果脚本较长,还可以使用redis-cli--eval直接执行文件。
eval命令和--eval参数本质是一样的,客户端如果想执行Lua脚本,
首先在客户端编写好Lua脚本代码,然后把脚本作为字符串发送给服务端,服务端会将执行结果返回给客户端。
(2)evalsha
除了使用eval,Redis还提供了evalsha命令来执行脚本。
首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和,
evalsha命令使用SHA1作为参数可以直接执行对应的Lua脚本,避免每次发送Lua脚本的开销。
这样客户端就不需要每次执行脚本内容,而脚本也会常驻在服务端,脚本功能得到复用。
加载脚本:script load命令可以将脚本内容加载到Redis内存中,例如:
127.0.0.1:6379> script load "return 'hello kebi'" "8df13960c2be12fec0947206c9e087cd68b2f1b9"
执行脚本:evalsha可以执行脚本:
127.0.0.1:6379> evalsha "8df13960c2be12fec0947206c9e087cd68b2f1b9" 0 "hello kebi"
2.Lua的Redis API
Lua可以使用redis.call函数实现对Redis的访问:
127.0.0.1:6379> eval 'return redis.call("get",KEYS[1])' 1 hello "world"
除此之外Lua还可以使用Redis.pcall函数实现对Redis的调用,redis.call和redis.pcall的不同在于,
如果redis.call执行失败,那么脚本执行结束会直接返回错误,而redis.pcall会忽略错误继续执行脚本,
所在实际开发中要根据具体的应用场景进行函数的选择。
Lua可以使用redis.log函数将Lua脚本的日志输出到Redis的日志文件中,但是一定要控制日志级别。
3.Redis如何管理Lua脚本
Redis提供了4个命令实现对Lua脚本的管理,下面分别介绍:
(1)script load
script load script
将Lua脚本加载到Redis内存之中。
(2)script exists
scripts exists sha1 [sha1 ...]
判断sha1是否已经加载到Redis内存之中。
127.0.0.1:6379> script load "return 'hello kebi'" "8df13960c2be12fec0947206c9e087cd68b2f1b9" 127.0.0.1:6379> script exists "8df13960c2be12fec0947206c9e087cd68b2f1b9" 1) (integer) 1
(3)script flush
script flush
清除Redis内存中已经加载的所有Lua脚本。
127.0.0.1:6379> script exists "8df13960c2be12fec0947206c9e087cd68b2f1b9" 1) (integer) 1 127.0.0.1:6379> script flush OK 127.0.0.1:6379> script exists "8df13960c2be12fec0947206c9e087cd68b2f1b9" 1) (integer) 0
(4)script kill
script kill
此命令用于杀掉正在执行的Lua脚本。
如果Lua脚本比较耗时,甚至Lua脚本存在问题,那么此时Lua脚本的执行会阻塞Redis,直到脚本执行完毕或外部进行干预将其结束。
下面模拟一个Lua脚本阻塞的情况进行说明。
执行Lua脚本,当前客户端会阻塞:
127.0.0.1:6379> eval 'while 1==1 do end' 0 ..卡在这里不动了,因为这个脚本一直做死循环
在另外一个客户端执行一个命令:
127.0.0.1:6379> get world (error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
此时Redis已经阻塞,无法处理正常的调用,可以使用script kill结束当前正在执行的Lua脚本:
127.0.0.1:6379> script kill OK
可在阻塞的那一端看到(脚本被杀死的信息):
127.0.0.1:6379> eval 'while 1==1 do end' 0 (error) ERR Error running script (call to f_c045d3ae3b3eca855e00c772db40aa560b3a1fc8): @user_script:1: Script killed by user with SCRIPT KILL... (38.91s)
注意:如果当前Lua脚本已经执行过写操作,那么script kill将不会生效。例如:
127.0.0.1:6379> eval 'while 1==1 do redis.call("set","k","v") end' 0 --一直执行写操作
在另一个客户端使用script kill杀不掉:
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 termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.
此时唯一的办法就是重启服务,必须使用shutdown nosave:
--不能以这种方法关 [root@Redis ~]# redis-cli shutdown (error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE. --必须这样 127.0.0.1:6379> shutdown nosave not connected> 然后再开
补充:
Redis提供了一个lua-time-limit参数,默认是5秒,它是Lua脚本的“超时时间”,但这个超时时间仅仅是当Lua脚本超过lua-time-limit后,向其他命令发送BUSY的信号,但是不会停止掉服务端和客户端的执行脚本。
4.使用Lua对Redis中的数据进行处理(案例)
Lua脚本功能为Redis开发和运维人员带来如下三个好处:
- Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令;
- Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这些命令常驻在Redis内存中,实现复用的的效果;
- Lua脚本可以将多条命令一次性打包,有效的减少网络开销。
下面以一个例子说明·Lua脚本的使用,当前列表记录着热门用户的id,假设这个列表有5个元素,如下所示:
127.0.0.1:6379> lrange hot:user:list 0 -1 1) "user:1:ratio" 2) "user:8:ratio" 3) "user:3:ratio" 4) "user:4:ratio" 5) "user:72:ratio"
user:{id}:ratio代表用户的热度,它本身又是一个字符串类型的键:
127.0.0.1:6379> mget user:1:ratio user:8:ratio user:3:ratio user:4:ratio user:72:ratio
1) "987"
2) "763"
3) "556"
4) "400"
5) "101"
现要求将列表内所有的键对应热度做加1操作,并且保证是原子性执行,此功能可以利用Lua脚本来实现。
脚本内容如下:
--将列表中所有元素取出,赋值给mylist local mylist = redis.call("lrange",KEYS[1],0,1) --定义局部变量count=0,这个count就是最后incr的总次数 local count = 0 --遍历mylist中所有元素,每次做完count自增,最后返回count for index,key in ipairs(mylist) do redis.call("incr",key) count = count + 1 end return count
执行脚本:
[root@Redis script.lua]# redis-cli --eval 3_1.lua hot:user:list
(integer) 5
执行后用户热度自增1:
127.0.0.1:6379> mget user:1:ratio user:8:ratio user:3:ratio user:4:ratio user:72:ratio
1) "987"
2) "763"
3) "557"
4) "401"
5) "101"