Lua是一种高效的轻量级脚本语言,可以方便地嵌入到其余语言中使用。在Redis中,借助Lua脚本能够自定义扩展命令。redis
Lua的变量分为全局变量和局部变量,全局变量无需声明就能够直接使用,默认值是nil。
全局变量:docker
a=1 -- 为全局变量a赋值 print(b) -- 无需声明便可使用,默认值是nil
局部变量:数据库
local c -- 声明一个局部变量c,默认值是nil local d=1 -- 声明一个局部变量d并赋值为1 local e,f -- 能够同时声明多个局部变量
但在Redis中,为了防止脚本之间相互影响,只容许使用局部变量。数组
Lua支持多重赋值,如:缓存
local a,b=1,2 --a的值是1,b的值是2 local c,d=1,2,3 --c的值是1,d的值是2,3被舍弃了 local e,f =1 --e的值是1,f的值是nil
数学操做符,包括常见的+ - * \ %(取模) -(一元操做符,取负)和幂运算符号^。安全
比较操做符,包括== ~=(不等于) > < >= <=。
比较操做符不会对两边的操做数进行自动类型转换:服务器
pring(1=='1') --结果为false print({'a'}=={'a'}) -false,表类型比较的是两者的引用
print(1 and 5) --5 print(1 or 5) --1 print(not 0) --false print('' or 1) --''
只要操做数不是nil或false,逻辑操做符就认为操做数是真,不然是假。并且即便是0或空字符串也被看成真,因此上面的代码中print(not 0)的结果为false,print('' or 1)的结果为''。数据结构
链接操做符
Lua中的链接操做符为'..',用来链接两个字符串。dom
取长度操做符函数
print(#'hello') --5
Lua中if语句的格式为
if condition then ... else if condition then ... else ... end
因为Lua中只有nil和false才认为是假,这里也须要注意避坑,好比Redis中EXISTS命令返回1和0分别表示存在或不存在,相似下面的写法if条件将始终为true:
if redis.call('EXISTS','key1') then ...
因此须要写成:
if redis.call('EXISTS','key1')==1 then ...
Lua中的循环语句有四种形式:
while condition do ... end
repeat ... until condition
for i=初值, 终值, 步长 do ... end
其中步长为1时能够省略。
for 变量1,变量2,...,变量N in 迭代器 do ... end
表是Lua中惟一的数据结构,能够理解为关联数组,除nil以外的任何类型的值均可以做为表的索引。
-- 表的定义 a={} --将变量a赋值为一个空表 -- 表的赋值 a['field']='value' --将field字段赋值为value print(a.field) --a['field']能够简化为a.field -- 定义的同时赋值 b={ name='bom', age=7 } -- 取值 print(b['age']) print(b.age)
当索引为整数的时候表和传统的数组同样,但须要注意的是Lua的索引是从1开始的。
a={} a[1]='bob' a[2]='daffy'
上面的定义和赋值的过程能够直接简化为:
a={'bob','daffy'}
取值:
print(a[1])
以前介绍的这种类型的for循环能够用于表的遍历:
for 变量1,变量2,...,变量N in 迭代器 do ... end
a={'bob','daffy'} for index,value in ipairs(a) do print(index) print(value) end
ipairs用于数组的遍历,index和value分别为元素的索引和值,变量名不是必须为index和value,能够自定义。
或者:
for i=1, #a do print(i) print(a[i]) end
经过#a能够去到数组a的长度。
对于非数组的遍历,可使用pairs
b={ name='bom', age=7 } for key,value in pairs(b) do print(key) print(value) end
变量名不是必须为key和value,能够自定义。
函数的定义为:
function(参数列表) ... end
实际使用中能够将其赋值给一个局部变量,如:
local square=function(num) return num * num end
还能够简化为:
local function square(num) return num * num end
若是实参的个数小于形参的个数,则没有匹配到的形参的值为nil;若是实参的个数大于形参的个数,则多出的实参会被忽略。若是但愿参数可变,能够用...表示形参。
在脚本中使用redis.call能够调用Redis命令
redis.call('SET','foo','bar')
redis.call的返回值就是Redis命令的执行结果。针对Redis的不一样返回类型,redis.call会将其转换为对应的Lua的数据类型,二者的对应关系为:
Redis返回类型 | Lua数据类型 |
---|---|
整数回复 | 数字类型 |
字符串回复 | 字符串类型 |
多行字符串回复 | 表类型(数组形式) |
状态回复 | 表类型(只有一个ok字段存储状态信息) |
错误回复 | 表类型(只有一个err字段存储错误信息) |
Redis的nil回复会被转换为false。
Lua脚本执行完毕后能够经过return将结果返回给Redis客户端,这是又会将Lua的数据类型转换为Redis的返回类型,过程与上面的表格相反。
redis.pcall函数与redis.call的功能相同,但redis.pcall在执行出错时会记录错误并继续执行,而redis.call则会中断执行。
在Redis客户端经过EVAL命令能够调用脚本,其格式为:
EVAL 脚本内容 key参数的数量 [key...] [arg...]
例如用脚原本设置键的值,就是这样的:
EVAL "return redis.call('SET',KEYS[1],ARGV[1])" 1 foo bar
经过key和arg这两类参数向脚本传递数据,它们的值能够在脚本中分别使用KEYS和ARGV两个表类型的全局变量访问。key参数的数量是必须指定的,没有key参数时必须设为0,EVAL会依据这个数值将传入的参数分别存入KEYS和ARGV两个表类型的全局变量。
若是脚本比较长,每次调用脚本都将整个脚本传给Redis会占用较多的带宽。而使用EVALSHA命令能够脚本内容的SHA1摘要来执行脚本,该命令的用法和EVAL同样,只不过是将脚本内容替换成脚本内容的SHA1摘要。Redis在执行EVAL命令时会计算脚本的SHA1摘要并记录在脚本缓存中,执行EVALSHA命令时Redis会根据提供的摘要从脚本缓存中查找对应的脚本内容,若是找到了则执行脚本,不然会返回错误:“NOSCRIPT No matching script. Please use EVAL.”。
具体使用时,能够先计算脚本的SHA1摘要,并用EVALSHA命令执行脚本,若是返回NOSCRIPT错误,就用EVAL从新执行脚本。
前面提到过向脚本传递的参数分为KEYS和ARGV两类,前者表示要操做的键名,后者表示非键名参数。但这一要求并不输强制的,好比设置键值的脚本:
EVAL "return redis.call('SET',KEYS[1],ARGV[1])" 1 foo bar
也能够写成:
EVAL "return redis.call('SET',ARGV[1],ARGV[2])" 0 foo bar
虽然规则不是强制的,但不遵照这样的规则可能会为后续带来没必要要的麻烦。好比Redis 3.0以后支持集群功能,开启集群后会将键发布到不一样的节点上,因此在脚本执行前就须要知道脚本会操做哪些键以便找到对应的节点,而若是脚本中的键名没有使用KEYS参数传递则没法兼容集群。
Redis限制脚本只能在沙盒中运行,只容许脚本对Redis的数据进行处理,而禁止使用Lua标准库中与文件或系统调用相关的函数,Redis还经过禁用脚本的全局变量的方式保证每一个脚本都是相对隔离、不会互相干扰的。
使用沙盒一方面可保证服务器的安全性,还可确保能够重现(脚本执行的结果只和脚本自己以及传递的参数有关)。
Redis还替换了math.random和math.randomseed函数,使得每次执行脚本时生成的随机数列都相同。若是但愿得到不一样的随机数序列,能够采用提早生成随机数并经过参数传递给脚本,或者提早生成随机数种子的方式。
集合类型和散列类型的字段是无序的,因此SMEMBERS和HKEYS命令本来会返回随机结果,但在脚本中调用这些命令时,Redis会对结果按照字典顺序排序。
对于会产生随机结果但没法排序的命令,好比SPOP,SRANDMEMBER, RANDOMKEY, TIME,Redis会在这类命令执行后将该脚本状态标记为lua_random_dirty,此后只容许调用只读命令,不容许修改数据库的值,不然会返回错误:“Write commands not allowed after non deterministic commands.”
EVAL命令会执行脚本,并将脚本计算SHA一、加入到脚本缓存中,若是只是但愿缓存脚本而不执行,就可使用SCRIPT LOAD,返回值是脚本的SHA1结果:
> SCRIPT LOAD "return redis.call('SET',KEYS[1],ARGV[1])" "cf63a54c34e159e75e5a3fe4794bb2ea636ee005"
经过SHA1查询某个脚本是否被缓存,能够查询多个SHA1。参数必须是完整的SHA1,而不能像docker只输前几位。返回结果1表示存在。
Redis将脚本加入到缓存后会永久保留,若是要清空缓存可使用SCRIPT FLUSH。
用于终止正在执行的脚本
Redis的脚本执行是原子的,脚本执行期间其余命令不会被执行,必须等待上一个脚本执行完成。
但为了防止某个脚本执行时间过长致使Redis没法提供服务(好比陷入死循环),Redis提供了lua-time-limit参数限制脚本的最长运行时间,默认为5秒钟。当脚本运行时间超过这一限制后,Redis将开始接受其余命令,但为了确保脚本的原子性,新的脚本仍然不会执行,而是会返回“BUSY”错误。
能够打开两个redis-cli实例A和B来验证,首先在A执行一个死循环脚本:
EVAL "while true do end" 0
这时在实例B执行GET key1会返回:
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
若是按照错误提示,在B执行SCRIPT KILL,这时在实例A的脚本会被终止,并返回:
(error) ERR Error running script (call to f_694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7): @user_script:1: Script killed by user with SCRIPT KILL...
但若是A已经对Redis的数据作了修改,则SCRIPT KILL没法将其终止,A执行:
EVAL "redis.call('SET','foo','bar') while true do end" 0
若是在B尝试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命令强行终止Redis。SHUTDOWN NOSAVE与SHUTDOWN命令的区别在于,SHUTDOWN NOSAVE将不会进行持久化操做,全部发生在上一次快照后的数据库修改都会丢失!