之前文章中提到过,redis官方在考虑lua代替事务,与事务相比,lua脚本也具有原子性,并且lua脚本在执行的时候,也会阻塞其他脚本或者redis命令并发执行,所以要尽量避免执行时间很长的脚本。

另外还要注意之前提到过的lua脚本导致内存泄漏问题,如果一组命令中的第一个就导致内存超出,那么这段lua脚本不会运行,但是第二个及之后的命令导致内存超出,redis是不会报错停止的,它会将整段脚本执行完(这也是其原子性的体现)

在Lua脚本执行期间,不执行任何 key 过期操作。当一个Lua脚本运行时,从概念上讲,master 中的时间是被冻结的,这样脚本运行的时候,一个给定的键要么存在要么不存在。这可以防止 key 在脚本中间过期,保证将相同的脚本发送到 slave ,从而在二者的数据集中产生相同的效果。

与事务只在所有语句执行完才能看到返回信息不同,lua可以在运行途中输出执行结果,好根据此结果判断接下来要干啥(if),还支持客户端缓存lua语句,这可以节省带宽,加快执行速度。

lua脚本可以嵌入多种语言,体积小巧,在php中可以作为一个扩展安装运行:https://www.php.net/manual/zh/book.lua.php (用户笔记0和1介绍php7以上版本安装lua2.0.7) https://blog.csdn.net/weixin_29504987/article/details/115501223

纯函数

脚本应该被写成纯函数,对于同样的数据集输入,给定相同的参数,脚本执行的 redis 写命令总是相同的。脚本执行的操作不能依赖于任何隐藏(非显式)数据,不能依赖于脚本在执行过程中、或脚本在不同执行时期之间可能变更的状态,并且它也不能依赖于任何来自 I/O 设备的外部输入。

  • Lua 没有访问系统时间或者其他内部状态的命令。

  • 在执行随机命令之后(比如randomkey、srandmember key [count]、time),接着执行可以修改数据集的 Redis 命令,Redis 会返回一个错误。如果脚本只是执行只读操作,那么就没有这一限制。注意,随机命令并不一定就指那些带 rand 字眼的命令,任何带有非确定性的命令都会被认为是随机命令,比如time。

  • 对无序返回默认(slient)进行字典排序。比如在 Redis 命令行客户端中直接执行smembers key,返回的元素是无序的,但是,假如在脚本中执行 redis.call("smembers", KEYS[1]) ,那么返回的总是排过序的元素。

  • 对 Lua 的伪随机数生成函数 math.random 和 math.randomseed 进行修改,每次运行脚本时,math.random 产生的随机数序列总是相同的。( math.randomseed 可以为脚本添加一个额外的参数,让这个参数作为 Lua 的随机数生成器的 seed 值,这样的话,只要给脚本传入不同的 seed ,脚本就会生成不同的列表元素。(redis 实现保证 math.random 和 math.randomseed 的输出和运行 Redis 的系统架构无关,无论是 32 位还是 64 位系统,无论是小端(little endian)还是大端(big endian)系统,这两个函数的输出总是相同的)。

全局变量保护

为了防止不必要的数据泄漏进 Lua 环境, redis 脚本不允许创建全局变量。如果一个脚本需要在多次执行之间维持某种状态,它应该使用 redis key 来进行状态保存。

一旦用户在脚本中混入了 Lua 全局状态,那么 AOF 持久化和复制(replication)都会无法保证,可以将脚本中用到的所有变量都使用 local 关键字定义为局部变量。

例1:

运行

eval、evalsha

lua脚本运行主要有两种方式,eval和evalsha。

eval:直接运行lua脚本

evalsha:根据给定的sha1校验码,对缓存在服务器中的脚本进行求值。redis包含一组将脚本写入缓存、清除、判断是否存在的script命令,也包含杀死正在运行脚本的命令。

虽然redis不会每次重新编译脚本,但是用evalsha可以减少脚本数据传递,减轻带宽压力。

redis.call()、redis.pcall()

执行redis命令的函数有两种:

redis.call():在运行中出错时停止运行并返回错误信息

redis.pcall():出错时并不引发(raise)错误,而是返回一个带 err 域的 Lua 表(table),用于表示错误

可以在命令行直接运行简单脚本:

1是指一个键名参数KEYS,之后的是附加参数ARGV。

eval "return redis.call('set',KEYS[1],ARGV[1])"  1 name ronaldo

可以执行lua脚本文件

例2:test.lua

--[[
 多行注释
 --]]
local mykey = KEYS[1]
local mykey2 = KEYS[2]
local myvalue = ARGV[1]
local myvalue2 = ARGV[2]
return {mykey,mykey2,myvalue,myvalue2}

执行:因为本地运行,redis-cli --eval  脚本路径/test.lua  -h -p -a都是默认可以省略。之后的参数中,‘,’之前的叫KEY,可以由KEY[1],KEY[2]...获取,‘,’之后的叫ARGV,由ARGV[1]...获取。注意‘,’要前后有空格

只有一个return,需要返回多个值需要包含在{}中(这实际上是lua的table数据类型,是一个关联数组),可以自定义格式,例2中的返回可以改为

return {mykey,{myvalue},mykey2,{myvalue2}}

例3:

test.lua

redis.call('set','name',ARGV[1])
local myname = redis.call('get','name')
if myname == 'kaka' then
return 'just you'
else
return 'sorry,not you'
end

执行:

redis-cli --eval  脚本路径/test.lua   -h -p -a   KEY[1]  KEY[2]...    ,   ARGV[1]   ARGV[2] ...

本例中未用到KEY,‘,’前空格即可,直接写AGRV参数

更多语法参考:https://www.runoob.com/lua/lua-basic-syntax.html  需要注意执行redis语句的lua脚本的限制

还要注意集群分区问题

沙箱(sandbox)和最大执行时间

脚本应该仅仅用于传递参数和对 Redis 数据进行处理,它不应该尝试去访问外部系统(比如文件系统),或者执行任何系统调用。

除此之外,脚本还有一个最大执行时间限制,它的默认值是 5 秒钟,一般正常运作的脚本通常可以在几分之几毫秒之内完成,这个限制主要是为了防止因编程错误而造成的无限循环而设置的。

当一个脚本达到最大执行时间的时候,为了保证其原子性,它并不会自动被 Redis 结束,当脚本超时:

  • Redis 记录一个脚本正在超时运行

  • Redis 开始重新接受其他客户端的命令请求,但是只有script kill和shutdown nosave两个命令会被处理,对于其他命令请求, Redis 服务器只是简单地返回 BUSY 错误。

  • 可以使用 script kill命令将一个仅执行只读命令的脚本杀死,因为不修改数据,因此杀死这个脚本并不破坏数据的完整性

  • 如果脚本已经执行过写命令,那么唯一允许执行的操作就是shutdown nosave ,它通过停止服务器来阻止当前数据集写入磁盘

流水线(pipeline)上下文(context)中的evalsha

之前我们提到过,redis事务底层实际上是使用pipeline以提高性能。

在流水线中,必须保证命令的执行顺序。一旦在流水线中因为 EVALSHA 命令而发生 NOSCRIPT 错误,那么这个流水线就再也没有办法重新执行了,否则的话,命令的执行顺序就会被打乱。

为了防止出现以上所说的问题,客户端库实现应该实施以下的其中一项措施:

  • 总是在流水线中使用eval命令

  • 检查流水线中要用到的所有命令,找到其中的eval命令,并使用 SCRIPT EXISTS sha1 [sha1 …] 命令检查要用到的脚本是不是全都已经保存在缓存里面了。如果所需的全部脚本都可以在缓存里找到,那么就可以放心地将所有 EVAL 命令改成 EVALSHA 命令,否则的话,就要在流水线的顶端(top)将缺少的脚本用 SCRIPT LOAD script 命令加上去。

lua语法入门

八大数据类型

nil

无效值、空值,在表达式中相当于false,对于全局变量和 table,nil 还有一个"删除"作用,给全局变量或者 table 表里的变量赋一个 nil 值,等同于把它们删掉,遍历时可以发现nil值被删除了。

作比较时应加上“”:type(X)=="nil"  true,type(X)==nil  false。因为type()函数本身返回的是一个string。

boolean

false 和 nil 看作是 false,其他的都为 true,数字0也是true。

local a = 0
if a then
return '0 is true'
else
return '0 is not true'
end

number

默认只有double型

string

单双引号引用,和用[[   ]]引用字符串块

对一个数字字符串上进行算术操作时,Lua 会尝试将这个数字字符串转成一个数字

#string 计算string长度,返回数字,中文根据编码长度不同

table

table是一个"关联数组",索引可以是数字或者是字符串,用它可以实现数组,哈希表,集合,对象。

默认初始索引一般以 1 开始(这就是之前例子中的KEY[1])

local a = {}
for i = 1,10 do
a[i] = i
end
return a

function(函数)

函数可以存在变量里,函数也可以看做一个值。

通过闭包和table可以很方便地支持面向对象编程所需要的一些关键机制,比如数据抽象,虚函数,继承和重载等

thread(线程)

在 Lua 里,最主要的线程是协同程序(coroutine)。它跟线程(thread)差不多,拥有自己独立的栈、局部变量和指令指针,可以跟其他协同程序共享全局变量和其他大部分东西。

线程跟协程的区别:线程可以同时多个运行,而协程任意时刻只能运行一个,并且处于运行状态的协程只有被挂起(suspend)时才会暂停。

userdata(自定义类型)

userdata是用户自定义类型,用于将应用、c、c++等语言创建的类型保存在userdata里,如指针等。

 

 

 

参考:

http://redisdoc.com/script/eval.html

更多推荐