2023年11月27日发(作者:)

Redis⼊门(6-Lua脚本

Lua基本语法

表类型

函数

Redis执⾏脚本

KEYSARGV

沙盒与随机数

脚本相关命令

原⼦性和执⾏时间

Lua是⼀种⾼效的轻量级脚本语⾔,能够⽅便地嵌⼊到其他语⾔中使⽤。在Redis中,借助Lua脚本可以⾃定义扩展命令。

Lua基本语法

数据类型

(nil),没有赋值的变量或表的字段值都是nil

布尔(boolean)

数字(number),整数或浮点数

字符串(string),字符串可以⽤单引号或双引号表⽰,可以包含转义字符如n r

(table),表类型是Lua语⾔中唯⼀的数据结构,既可以当数组⼜可以当字典,⼗分灵活

函数(function),函数在Lua中是⼀等值(first-class-value),可以存储在变量中、作为函数的参数或返回结果。

变量

Lua的变量分为全局变量和局部变量,全局变量⽆需声明就可以直接使⽤,默认值是nil

全局变量:

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的值是1b的值是2

local c,d=1,2,3 --c的值是1d的值是23被舍弃了

local e,f =1 --e的值是1f的值是nil

操作符

1. 数学操作符,包括常见的+ - * %(取模) -(⼀元操作符,取负)和幂运算符号^

2. ⽐较操作符,包括== ~=(不等于) > < >= <=

⽐较操作符不会对两边的操作数进⾏⾃动类型转换:

pring(1=='1') --结果为false

print({'a'}=={'a'}) -false,表类型⽐较的是⼆者的引⽤

3. 逻辑操作符

包括下⾯三个:

not,根据操作数的真和假相应地返回falsetrue

anda and b中如果a是真则返回b,否则返回a

ora or b中,如果a是真则返回a,否则返回b

这些根据操作符短路的原理可以推断出。

print(1 and 5) --5

print(1 or 5) --1

print(not 0) --false

print('' or 1) --''

只要操作数不是nilfalse,逻辑操作符就认为操作数是真,否则是假。⽽且即使是0或空字符串也被当作真,所以上⾯的代码中print(not 0)

的结果为false,print('' or 1)的结果为''

4. 连接操作符

Lua中的连接操作符为'..',⽤来连接两个字符串。

5. 取长度操作符

print(#'hello') --5

if语句

Luaif语句的格式为

if condition then

...

else if condition then

...

else

...

end

由于Lua中只有nilfalse才认为是假,这⾥也需要注意避坑,⽐如RedisEXISTS命令返回10分别表⽰存在或不存在,类似下⾯的写法if

件将始终为true

if ('EXISTS','key1') then

...

所以需要写成:

if ('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']可以简化为

-- 定义的同时赋值

b={

name='bom',

age=7

}

-- 取值

print(b['age'])

print()

当索引为整数的时候表和传统的数组⼀样,但需要注意的是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⽤于数组的遍历,indexvalue分别为元素的索引和值,变量名不是必须为indexvalue,可以⾃定义。

或者:

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

变量名不是必须为keyvalue,可以⾃定义。

函数

函数的定义为:

function(参数列表)

...

end

实际使⽤中可以将其赋值给⼀个局部变量,如:

local square=function(num)

return num * num

end

还可以简化为:

local function square(num)

return num * num

end

如果实参的个数⼩于形参的个数,则没有匹配到的形参的值为nil;如果实参的个数⼤于形参的个数,则多出的实参会被忽略。如果希望参数

可变,可以⽤...表⽰形参。

Redis返回类型Lua数据类型

整数回复数字类型

字符串回复字符串类型

状态回复表类型(只有⼀个ok字段存储状态信息)

错误回复表类型(只有⼀个err字段存储错误信息)

多⾏字符串回复表类型(数组形式)

Redisnil回复会被转换为false

Lua脚本执⾏完毕后可以通过return将结果返回给Redis客户端,这是⼜会将Lua的数据类型转换为Redis的返回类型,过程与上⾯的表格相

反。

函数与的功能相同,但在执⾏出错时会记录错误并继续执⾏,⽽则会中断执⾏。

Redis执⾏脚本

EVAL

Redis客户端通过EVAL命令可以调⽤脚本,其格式为:

EVAL 脚本内容 key参数的数量 [] []

例如⽤脚本来设置键的值,就是这样的:

EVAL "return ('SET',KEYS[1],ARGV[1])" 1 foo bar

通过keyarg这两类参数向脚本传递数据,它们的值可以在脚本中分别使⽤KEYSARGV两个表类型的全局变量访问。key参数的数量是必

须指定的,没有key参数时必须设为0EVAL会依据这个数值将传⼊的参数分别存⼊KEYSARGV两个表类型的全局变量。

EVALSHA

如果脚本⽐较长,每次调⽤脚本都将整个脚本传给Redis会占⽤较多的带宽。⽽使⽤EVALSHA命令可以脚本内容的SHA1摘要来执⾏脚本,

该命令的⽤法和EVAL⼀样,只不过是将脚本内容替换成脚本内容的SHA1摘要。Redis在执⾏EVAL命令时会计算脚本的SHA1摘要并记录在

脚本缓存中,执⾏EVALSHA命令时Redis会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果找到了则执⾏脚本,否则会返回错

误:“NOSCRIPT No matching script. Please use EVAL.”

具体使⽤时,可以先计算脚本的SHA1摘要,并⽤EVALSHA命令执⾏脚本,如果返回NOSCRIPT错误,就⽤EVAL重新执⾏脚本。

KEYSARGV

前⾯提到过向脚本传递的参数分为KEYSARGV两类,前者表⽰要操作的键名,后者表⽰⾮键名参数。但这⼀要求并不输强制的,⽐如设

置键值的脚本:

EVAL "return ('SET',KEYS[1],ARGV[1])" 1 foo bar

也可以写成:

EVAL "return ('SET',ARGV[1],ARGV[2])" 0 foo bar

虽然规则不是强制的,但不遵守这样的规则可能会为后续带来不必要的⿇烦。⽐如Redis 3.0之后⽀持集群功能,开启集群后会将键发布到不

同的节点上,所以在脚本执⾏前就需要知道脚本会操作哪些键以便找到对应的节点,⽽如果脚本中的键名没有使⽤KEYS参数传递则⽆法兼

容集群。

沙盒与随机数

Redis限制脚本只能在沙盒中运⾏,只允许脚本对Redis的数据进⾏处理,⽽禁⽌使⽤Lua标准库中与⽂件或系统调⽤相关的函数,Redis

通过禁⽤脚本的全局变量的⽅式保证每个脚本都是相对隔离、不会互相⼲扰的。

使⽤沙盒⼀⽅⾯可保证服务器的安全性,还可确保可以重现(脚本执⾏的结果只和脚本本⾝以及传递的参数有关)。

Redis还替换了seed函数,使得每次执⾏脚本时⽣成的随机数列都相同。如果希望获得不同的随机数序列,可

以采⽤提前⽣成随机数并通过参数传递给脚本,或者提前⽣成随机数种⼦的⽅式。

集合类型和散列类型的字段是⽆序的,所以SMEMBERSHKEYS命令原本会返回随机结果,但在脚本中调⽤这些命令时,Redis会对结果

按照字典顺序排序。

对于会产⽣随机结果但⽆法排序的命令,⽐如SPOPSRANDMEMBER, RANDOMKEY, TIMERedis会在这类命令执⾏后将该脚本状态

标记为lua_random_dirty,此后只允许调⽤只读命令,不允许修改数据库的值,否则会返回错误:“Write commands not allowed after non

deterministic commands.”

脚本相关命令

SCRIPT LOAD

EVAL命令会执⾏脚本,并将脚本计算SHA1、加⼊到脚本缓存中,如果只是希望缓存脚本⽽不执⾏,就可以使⽤SCRIPT LOAD,返回值是

脚本的SHA1结果:

> SCRIPT LOAD "return ('SET',KEYS[1],ARGV[1])"

"cf63a54c34e159e75e5a3fe4794bb2ea636ee005"

SCRIPT EXISTS

通过SHA1查询某个脚本是否被缓存,可以查询多个SHA1。参数必须是完整的SHA1,⽽不能像docker只输前⼏位。返回结果1表⽰存在。

SCRIPT FLUSH

Redis将脚本加⼊到缓存后会永久保留,如果要清空缓存可以使⽤SCRIPT FLUSH

SCRIPT KILL

⽤于终⽌正在执⾏的脚本

原⼦性和执⾏时间

Redis的脚本执⾏是原⼦的,脚本执⾏期间其他命令不会被执⾏,必须等待上⼀个脚本执⾏完成。

但为了防⽌某个脚本执⾏时间过长导致Redis⽆法提供服务(⽐如陷⼊死循环),Redis提供了lua-time-limit参数限制脚本的最长运⾏时间,

默认为5秒钟。当脚本运⾏时间超过这⼀限制后,Redis将开始接受其他命令,但为了确保脚本的原⼦性,新的脚本仍然不会执⾏,⽽是会返

“BUSY”错误。

可以打开两个redis-cli实例AB来验证,⾸先在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

但如果A已经对Redis的数据做了修改,则SCRIPT KILL⽆法将其终⽌,A执⾏:

EVAL "('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命令强⾏终⽌RedisSHUTDOWN NOSAVESHUTDOWN命令的区别在于,SHUTDOWN

NOSAVE将不会进⾏持久化操作,所有发⽣在上⼀次快照后的数据库修改都会丢失!