Redis:18---常用功能之(Lua脚本)
为了保证多条命令组合的原子性,Redis提供了简单的事务功能以及集成Lua脚本来解决这个问题,本文介绍Lua,事务已经在前一篇文章介绍过了
一、Lua概述
Lua语言是在1993年由巴西一个大学研究小组发明,其设计目标是作为嵌入式程序移植到其他应用程序,它是由C语言实现的,虽然简单小巧但是功能强大
所以许多应用都选用它作为脚本语言,尤其是在游戏领域,例如大名鼎鼎的暴雪公司将Lua语言引入到“魔兽世界”这款游戏中,Rovio公司将 Lua语言作为“愤怒的小鸟”这款火爆游戏的关卡升级引擎,Web服务器Nginx 将Lua语言作为扩展,增强自身功能
Redis将Lua作为脚本语言可帮助开发者定制自己的Redis命令,在这之前,必须修改源
二、Lua的基本语法
现在先简单地介绍一下Lua的基本语法(只介绍部分语法),与Redis无关,纯属于Lua的语法
数据类型
Lua语言提供了如下几种数据类型:booleans(布尔)、numbers(数值)、strings(字符串)、tables(表格),和许多高级语言相比,相对简单
全局变量/局部变量
local代表val是一个局部变量,如果没有local代表是全局变量
local strings val = "world"
字符串
下面定义一个字符串类型的数据:
local strings val = "world"
print函数可以打印出变量的值,例如下面代码将打印world
local strings val = "world"
printf(val)
注释
"--"是Lua语言的注释
-- 注释
数组
在Lua中,如果要使用类似数组的功能,可以用tables类型
下面代码使用定义了一个tables类型的变量myArray,但和大多数编程语言不同的是, Lua的数组下标从1开始计算:
local tables myArray = {"redis", "jedis", true, 88.0}
-- true
print(myArray[3])
for
下面代码会计算1到100的和,关键字for以end作为结束符:
local int sum = 0
for i = 1, 100
do
sum = sum + i
end
-- 输出结果为5050
print(sum)
要遍历myArray,首先需要知道tables的长度,只需要在变量前加一个# 号即可:
for i = 1, #myArray
do
print(myArray[i])
end
除此之外,Lua还提供了内置函数ipairs,使用for index,value ipairs(tables)可以遍历出所有的索引下标和值:
for index,value in ipairs(myArray)
do
print(index)
print(value)
end
while
下面代码同样会计算1到100的和,只不过使用的是while循环,while循环同样以end作为结束符
local int sum = 0
local int i = 0
while i <= 100
do
sum = sum +i
i = i + 1
end
--输出结果为5050
print(sum)
if、else
要确定数组中是否包含了jedis,有则打印true,注意if以end结尾,if后 紧跟then:
local tables myArray = {"redis", "jedis", true, 88.0}
for i = 1, #myArray
do
if myArray[i] == "jedis"
then
print("true")
break
else
--do nothing
end
end
哈希
如果要使用类似哈希的功能,同样可以使用tables类型
例如下面代码 定义了一个tables,每个元素包含了key和value,其中strings1..string2是将两个字符串进行连接:
local tables user_1 = {age = 28, name = "tome"}
--user_1 age is 28
print("user_1 age is " .. user_1["age"])
如果要遍历user_1,可以使用Lua的内置函数pairs:
for key,value in pairs(user_1)
do print(key .. value)
end
函数定义
在Lua中,函数以function开头,以end结尾,funcName是函数名,中间部分是函数体
function funcName()
...
end
-- contact函数将两个字符串拼接:
function contact(str1, str2)
return str1 .. str2
end
--"hello world"
print(contact("hello ", "world"))
三、在Redis中使用Lua(eval、evalsha)
在Redis中执行Lua脚本有两种方法:eval和evalsha
eval
EVAL命令可以直接执行Lua脚本
格式如下:
eval 脚本内容 key个数 key列表 参数列表
例如:下面执行一个Lua脚本,内容为“hello world”,参数为0个
例如:下面使用了key列表和参数列表来为Lua脚本提供更多的灵活性:
此时KEYS[1]="redis",ARGV[1]="world",所以最终的返回结果是"hello redisworld"。
eval 'return "hello " .. KEYS[1] .. ARGV[1]' 1 redis world
如果Lua脚本较长,还可以使用redis-cli --eval直接执行文件。eval命令和--eval参数本质是一样的,客户端如果想执行Lua脚本,首先在客户端编写好Lua脚本代码,然后把脚本作为字符串发送给服务端,服务端会将执行结果返回给客户端,整个过程如下图所示:
evalsha
除了使用eval,Redis还提供了evalsha命令来执行Lua脚本
格式如下:
evalsha 脚本SHA1值 key个数 key列表 参数列表
执行原理如下:
首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和
evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本,避免每次发送Lua脚本的开销。这样客户端就不需要每次执行脚本内容,而脚本也会常驻在服务端,脚本功能得到了复用
演示案例:
建立一个名为lua_get.lua的脚本文件,内容如下:
return "hello " .. KEYS[1] .. ARGV[1]
加载脚本:在系统命令行执行script load命令(下面会介绍)可以将脚本内容加载到Redis内存中,例如下面将lua_get.lua加载到Redis中,得到SHA1 为:"7413dc2440db1fea7c0a0bde841fa68eefaf149c"
redis-cli script load "$(cat lua_get.lua)"
执行脚本:所以在redis-cli内部使用上面的SHA1值就可以执行该脚本了
evalsha 7413dc2440db1fea7c0a0bde841fa68eefaf149c 1 redis world
四、Lua中的Redis API
下面介绍几个在Lua中可以使用的Redis API,更多的API可以百度自行查阅
call()
Lua可以使用redis.call函数实现对Redis的访问
例如,下面代码是Lua使用redis.call调用了Redis的set和get操作:
redis.call("set", "hello", "world")
redis.call("get", "hello")
在Redis中执行的效果如下:
pcall()
除此之外Lua还可以使用redis.pcall函数实现对Redis的调用
redis.call和 redis.pcall的不同在于:如果redis.call执行失败,那么脚本执行结束会直接返 回错误,而redis.pcall会忽略错误继续执行脚本,所以在实际开发中要根据 具体的应用场景进行函数的选择。
log()
Lua可以使用redis.log函数将Lua脚本的日志输出到Redis的日志文件中, 但是一定要控制日志级别
备注:Redis3.2提供了Lua Script Debugger功能用来调试复杂的Lua脚本,具体 可以参考:http://redis.io/topics/ldb
五、使用案例
Lua脚本功能为Redis开发和运维人员带来如下三个好处:
Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令
Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这些命令常驻在Redis内存中,实现复用的效果
Lua脚本可以将多条命令一次性打包,有效地减少网络开销
演示案例
①当前列表记录着热门用户的id, 假设这个列表有5个元素,如下所示
RPUSH host:user:list user:1:ratio user:8:ratio user:3:ratio user:99:ratio user:72:ratio
lrange host:user:list 0 -1
user:{id}:ratio代表用户的热度,它本身又是一个字符串类型的键:
set user:1:ratio 986
set user:8:ratio 762
set user:3:ratio 556
set user:99:ratio 400
set user:72:ratio 101
②现要求将列表内所有的键对应热度做加1操作,并且保证是原子执行, 此功能可以利用Lua脚本来实现,例如下面是一个名为lrange_and_mincr.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
③并执行如下操作,返回结果为5:
redis-cli --eval lrange_and_mincr.lua host:user:list
④执行后所有用户的热度自增1:
mget user:1:ratio user:8:ratio user:3:ratio user:99:ratio user:72:ratio
六、Redis管理Lua脚本
Redis提供了4个命令实现对Lua脚本的管理,下面分别介绍
①script load
script load script
此命令用于将Lua脚本加载到Redis内存中,上面evalsha命令的演示案例中有介绍
②script exists
scripts exists sha1 [sha2 …]
此命令用于判断sha1、sha2...是否已经加载到Redis内存中
例如,在上面evalsha命令介绍的演示案例中,我们加载了一个SHA1值为“7413dc2440db1fea7c0a0bde841fa68eefaf149c”的Lua脚本,现在我们进行检测,结果返回1
③script flush
script flush
此命令用于清除Redis内存已经加载的所有Lua脚本
例如:例如我们清楚上面那个SHA1值为“7413dc2440db1fea7c0a0bde841fa68eefaf149c”的脚本,执行script flush后,脚本不再存在了:
④script kill
script kill
此命令用于杀掉正在执行的Lua脚本
如果Lua脚本比较耗时,甚至Lua脚本存在问题,那么此时Lua脚本的执行会阻塞Redis,直到脚本执行完毕或者外部进行干预将其结束。例如,下面的Lua脚本会无线循环,因此客户端会阻塞
lua-time-limit参数:
Redis提供了一个lua-time-limit参数,单位为毫秒,默认为5000毫秒(5秒),它是Lua脚本的“超时时间”
但这个超时时间仅仅是当Lua脚本时间超过lua-time-limit后,向其他命令调用发送BUSY的信号,但是并不会停止掉服务端和客户端的脚本执行,所以当达到lua-time-limit值之后,其他客户端在执行正常的命令时,将会收到“Busy Redis is busy running a script”错误,并且提示使用script kill或者shutdown nosave命令来杀掉这个busy的脚本(见下图演示案例)
script kill比shutdown命令更好:在Lua阻塞时,使用script kill更好,因为shutdown会让Redis服务停止,而script不会,其只是关闭Lua脚本的执行
演示案例
接着上图,我们左侧的客户端执行的Lua脚本处于阻塞状态,此时我们在右边输入script kill杀死正在执行的Lua脚本,此时左侧客户端返回,所有客户端可以继续执行访问了
scritp kill命令失效的情况
如果Lua脚本正在执行写操作,那么script kill命令就会失效
例如,下面左侧客户端执行的Lua脚本一直不停的在执行set操作,右侧客户端使用script kill时会显示出错(上提示Lua脚本正在向Redis执行写命令,要么等待脚本执行结束要么使用shutdown nosave停掉Redis服务)