1. lua用法简述
Lua语言是在1993年由巴西一个大学研究小组发明,其设计目标是作为嵌入式程序移植到其他应用程序,它是由C语言实现的,虽然简单小巧但是功能强大,所以许多应用都选用它作为脚本语言,尤其是在游戏领域,例如大名鼎鼎的暴雪公司将Lua语言引入到“魔兽世界”这款游戏中,Rovio公司将Lua语言作为“愤怒的小鸟”这款火爆游戏的关卡升级引擎,Web服务器Nginx将Lua语言作为扩展,增强自身功能。Redis将Lua作为脚本语言可帮助开发者定制自己的Redis命令,在这之前,必须修改源码。在介绍如何在Redis中使用Lua脚本之前,有必要对Lua语言的使用做一个基本的介绍。
Lua语言提供了如下几种数据类型:booleans(布尔)、numbers(数值)、strings(字符串)、tables(表格),和许多高级语言相比,相对简单。下面将结合例子对Lua的基本数据类型和逻辑处理进行说明。
- 字符串
local strings val = "world"
其中,local代表val是一个局部变量,如果没有local代表是全局变量。print函数可以打印出变量的值,例如下面代码将打印world,其中"--"是Lua语言的注释。
-- 结果是 "world"
print(hello)
- 数组
在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"))
2. redis与lua
在Redis中执行Lua脚本有两种方法:eval和evalsha。
- eval
eval 脚本内容 key个数 key列表 参数列表
下面例子使用了key列表和参数列表来为Lua脚本提供更多的灵活性:
127.0.0.1:6379> eval 'return "hello " .. KEYS[1] .. ARGV[1]' 1 redis world
"hello redisworld"
此时KEYS[1]="redis",ARGV[1]="world",所以最终的返回结果是"hello redisworld"。
如果Lua脚本较长,还可以使用redis-cli--eval直接执行文件。eval命令和--eval参数本质是一样的,客户端如果想执行Lua脚本,首先在客户端编写好Lua脚本代码,然后把脚本作为字符串发送给服务端,服务端会将执行结果返回给客户端,整个过程如图所示。
- evalsha
除了使用eval,Redis还提供了evalsha命令来执行Lua脚本。如图所示,首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验值,evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本,避免每次发送Lua脚本的开销。这样客户端就不需要每次执行脚本内容,而脚本也会常驻在服务端,脚本功能得到了复用。
- 加载脚本:script load命令可以将脚本内容加载到Redis内存中,例如下面将lua_get.lua加载到Redis中,得到SHA1为:"7413dc2440db1fea7c0a0bde841fa68eefaf149c"
# redis-cli script load "$(cat lua_get.lua)"
"7413dc2440db1fea7c0a0bde841fa68eefaf149c"
执行脚本:evalsha的使用方法如下,参数使用SHA1值,执行逻辑和eval一致。
evalsha 脚本 SHA1 值 key个数 key列表 参数列表
所以只需要执行如下操作,就可以调用lua_get.lua脚本:
127.0.0.1:6379> evalsha 7413dc2440db1fea7c0a0bde841fa68eefaf149c 1 redis world
"hello redisworld"
3.lua中的redis API
Lua可以使用redis.call函数实现对Redis的访问,例如下面代码是Lua使用
redis.call调用了Redis的set和get操作:
redis.call("set", "hello", "world")
redis.call("get", "hello")
放在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的日志文件中,
但是一定要控制日志级别。
Redis3.2提供了Lua Script Debugger功能用来调试复杂的Lua脚本,具体
可以参考:http://redis.io/topics/ldb。
3. lua脚本好处
Lua脚本功能为Redis开发和运维人员带来如下三个好处:
- Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
- Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这些命令常驻在Redis内存中,实现复用的效果。
- Lua脚本可以将多条命令一次性打包,有效地减少网络开销。
4. 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 exists a5260dd66ce02462c5b5231c727b3f7772c0bcc5
1) (integer) 1
返回结果代表sha1[sha1…]被加载到Redis内存的个数。
(3)script flush
script flush
此命令用于清除Redis内存已经加载的所有Lua脚本,在执行script flush后,a5260dd66ce02462c5b5231c727b3f7772c0bcc5不再存在:
127.0.0.1:6379> script exists a5260dd66ce02462c5b5231c727b3f7772c0bcc5
1) (integer) 1
127.0.0.1:6379> script flush
OK
127.0.0.1:6379> script exists a5260dd66ce02462c5b5231c727b3f7772c0bcc5
1) (integer) 0
(4)script kill
script kill
此命令用于杀掉正在执行的Lua脚本。如果Lua脚本比较耗时,甚至Lua脚本存在问题,那么此时Lua脚本的执行会阻塞Redis,直到脚本执行完毕或者外部进行干预将其结束。下面我们模拟一个Lua脚本阻塞的情况进行说明。
下面的代码会使Lua进入死循环:
while 1 == 1
do
end
执行Lua脚本,当前客户端会阻塞:
127.0.0.1:6379> eval 'while 1==1 do end' 0
Redis提供了一个lua-time-limit参数,默认是5秒,它是Lua脚本的“超时时间”,但这个超时时间仅仅是当Lua脚本时间超过lua-time-limit后,向其他命令调用发送BUSY的信号,但是并不会停止掉服务端和客户端的脚本执行,所以当达到lua-time-limit值之后,其他客户端在执行正常的命令时,将会收到“Busy Redis is busy running a script”错误,并且提示使用script kill或者shutdown nosave命令来杀掉这个busy的脚本:
127.0.0.1:6379> get hello
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or
SHUTDOWN NOSAVE.
此时Redis已经阻塞,无法处理正常的调用,这时可以选择继续等待,但更多时候需要快速将脚本杀掉。使用shutdown save显然不太合适,所以选择script kill,当script kill执行之后,客户端调用会恢复:
127.0.0.1:6379> script kill
OK
127.0.0.1:6379> get hello
"world"
但是有一点需要注意,如果当前Lua脚本正在执行写操作,那么script kill将不会生效。例如,我们模拟一个不停的写操作:
while 1==1
do
redis.call("set","k","v")
end
此时如果执行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.
上面提示Lua脚本正在向Redis执行写命令,要么等待脚本执行结束要么使用shutdown save停掉Redis服务。可见Lua脚本虽然好用,但是使用不当破坏性也是难以想象的。