redis之lua脚本

目录

eval

语法

例子

lua中执行redis函数

redis.call()

redis.pcall()

为什么要用正确的形式来传递键?

转换规则

redis到lua

lua到redis

脚本的原子性

evalsha

语法

evalsha命令表现如下:

脚本缓存

刷新脚本缓存

script命令

纯脚本函数

全局变量保护

loglevel取值

沙箱和最大执行时间

当达到最大执行时间后,会执行以下操作

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

script load 

语法

script exists

语法

script flush

语法

script kill

语法


参考官方文档:http://redisdoc.com/script/index.html

eval

redis从2.6.0版本开始,通过内置的lua解释器,可以使用eval对lua脚本求值

语法

eval script numkeys key [key...] arg[arg...]

script:一段lua5.1脚本程序,会被运行在redis服务器上下文,这段脚本不变也不应该被定义成一个lua函数

numkeys:指定键名参数的个数

key[key...]:从eval的迪桑参数开始算起,表示在脚本中用到的那些redis键(key),这些键名参数可以在lua中通过全局变量KEYS数组,用1为基址的形式访问。

KEYS[1],KEYS[2]...

arg[arg...]:附加参数,可以在lua中通过全局变量ARGV数组方式

ARGV[1], ARGV[2]...

例子

> eval "return {KEYS[1], KEYS[2], ARGV[1], ARGV[2]}" 2 key1 key2 arg1 arg2
1)"key1"
2)"key2"
3)"arg1"
4)"arg2"

"return {KEYS[1], KEYS[2], ARGV[1], ARGV[2]}"是被求值的lua脚本,“2”指定了简明参数的数量,key1和key2是键名参数,分别使用KEYS[1],KEYS[2]访问,arg1和arg2是附加参数,通过ARGV[1]和ARGV[2]访问。

lua中执行redis函数

redis.call()

在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,错误的信息会说明造成错误的原因。

redis.pcall()

在执行命令出错时,并不会引发错误,而是返回一个带err域的lua表(table), 用来表示错误

> eval "return redis.call('set', KEYS[1], 'bar')" 1 foo  // 正确
> eval "return redis.call('set', 'foo', 'bar')" 0 // 错误。脚本里所有键都应该由KEYS数组来传递

为什么要用正确的形式来传递键?

不仅仅是eval命令,所有的redis命令,在执行前都会被分析,以此来确定命令会对哪些键进行操作。

对于eval命令来说,必须使用正确的形式来传递键,才能确保分析工作正常执行;

除此之外,确保redis集群可以将你的请求发送到正确的集群节点。

lua数据类型与redis数据类型的转换

当通过redis.call()或者redis.pcall()执行redis命令的时候,命令的返回值会被转换成lua数据结构。

当lua脚本在redis内置的解释器里面运行的时候,lua脚本的返回值会被转换成redis协议(protocol),然后由eval将值返回给客户端。

转换规则

redis到lua

Redis integer reply -> Lua number / Redis 整数转换成 Lua 数字
Redis bulk reply -> Lua string / Redis bulk 回复转换成 Lua 字符串
Redis multi bulk reply -> Lua table (may have other Redis data types nested) / Redis 多条 bulk 回复转换成 Lua 表,表内可能有其他别的 Redis 数据类型
Redis status reply -> Lua table with a single ok field containing the status / Redis 状态回复转换成 Lua 表,表内的 ok 域包含了状态信息
Redis error reply -> Lua table with a single err field containing the error / Redis 错误回复转换成 Lua 表,表内的 err 域包含了错误信息
Redis Nil bulk reply and Nil multi bulk reply -> Lua false boolean type / Redis 的 Nil 回复和 Nil 多条回复转换成 Lua 的布尔值 false

lua到redis

Lua number -> Redis integer reply / Lua 数字转换成 Redis 整数
Lua string -> Redis bulk reply / Lua 字符串转换成 Redis bulk 回复
Lua table (array) -> Redis multi bulk reply / Lua 表(数组)转换成 Redis 多条 bulk 回复
Lua table with a single ok field -> Redis status reply / 一个带单个 ok 域的 Lua 表,转换成 Redis 状态回复
Lua table with a single err field -> Redis error reply / 一个带单个 err 域的 Lua 表,转换成 Redis 错误回复
Lua boolean false -> Redis Nil bulk reply / Lua 的布尔值 false 转换成 Redis 的 Nil bulk 回复

lua到redis的额外的规则,没有从redis到lua转换的对应:

Lua boolean true -> Redis integer reply with value of 1 / Lua 布尔值 true 转换成 Redis 整数回复中的 1

脚本的原子性

redis使用单个lua解释器去运行所有脚本,并且redis也保证脚本会以原子性的方式执行,当某个脚本正在运行的时候,不会有其他脚本或者redis命令执行。

当然,如果是一个运行缓慢的脚本,就可能会因为服务器忙而无法执行其他命令。

evalsha

eval命令要求在每次执行脚本的时候都发送一次脚本主体(script body)。redis由一个内部缓存机制,不会每次都重新编译脚本。每次都发送脚本主体,会导致带宽的消耗。

为了减少带宽消耗,redis实现了evalsha命令,作用与eval一样,都是对脚本求值,但第一个参数不再是脚本,而是脚本的sha1校验和(sum)

语法

evalsha sha1 numkeys key[key ...] arg[arg ...]

evalsha命令表现如下:

1.如果服务器还记得给定的sha1校验和所指定的脚本,那么执行这个脚本

2.如果服务器不记得给定的sha1校验和所指定的脚本,那么它返回一个特殊的错误,提醒用户使用eval代替evalsha。

`NOSCRIPT` No matching script. Please use [EVAL]

脚本缓存

redis保证所有被运行过的脚本都会被永久保存在脚本缓存中,意味着当eval命令在一个redis实例上成功执行某个脚本之后,随后针对这个脚本的所有evalsha命令都会成功执行。

刷新脚本缓存

// 清空运行过的所有脚本缓存
script flush 

通常只有在云计算过程中,redis实例被改作其他客户端或者别的应用程序的实例时才会执行这个命令。

缓存可以长时间储存而不会产生内存问题,体积小,数量小。

不移除缓存脚本的好处:

对于一个和redis保持持久化链接的程序来说,它可以确信,执行过一次的脚本会一直保留在内存中,因此它可以在流水线中使用evalsha命令而不必担心因找不到脚本而产生错误。

script命令

用来对脚本子系统进行控制

// 清除所有脚本缓存
script flush
// 根据给定的脚本校验和,检查指定的脚本是否在缓存中存在
script exists sha1[sha1 ...]
// 将一个脚本装入脚本缓存,但并不立即运行
script load script
// kill正在运行的脚本
script kill

纯脚本函数

脚本应该被写成纯函数。脚本应该具有的属性:

对于同样的数据集输入,给定相同的参数,脚本执行的redis写命令总是相同的,脚本执行的曹祖不能依赖于任何隐藏的数据,不能依赖于脚本在执行过程中、脚本在不同执行时期之间可能变更的状态,并且也不能依赖任何来自I/O设备的外部输入。

使用系统时间、调用像randomkey随机命令、使用lua随机数,这些操作都会造成脚本的求值无法每次都得出同样的结果。

为了满足该属性,redis做了以下工作:

1.lua没有访问系统时间或者其他内部状态的命令
2.redis会返回一个错误,阻止这样的脚本运行:这些脚本在执行随机命令之后,还会执行修改数据集的redis命令
3.每当从lua脚本中调用那些返回无序元素的命令时,执行命令所得得数据在返回给lua之前会先执行一个静默得字典排序。
4.对lua得伪随机数生成函数math.random和math.randomseed进行修改,使得每次在运行新脚本得时候,总是拥有同样得seed值

全局变量保护

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

lua得debug工具或其他设施,比如打印alter用于实现全局保护得meta table都可以用来实现全局变量保护。

一旦在脚本混入了lua全局状态,那么aof持久化和复制(replication)都会无法保证,所以不要在脚本中使用全局变量。

避免引入全局变量:脚本中所有用到得变量都使用local关键字定义为局部变量。

redis内置得lua解释器加载了以下lua库

base, table,string, math,debug,cjson,cmsgpack

cjson库可以让lua非常快得处理json数据。

每个redis实例都保证会加载以上这些库,从而确保每个redis脚本得运行环境是相同得。

使用脚本记录redis日志

在lua脚本中,使用redis.log来记录redis日志。

// 在lua脚本中记录redis日志
redis.log(loglevel, message)

loglevel取值

redis.LOG_DEBUG
redis.LOG_VERBOSE
redis.LOG_NOTICE
redis.LOG_WARNING

这些日志级别与标准得redis日志等级相对应。

沙箱和最大执行时间

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

最大执行时间限制,默认是5秒。

一般正常运行得脚本通常可以在几分之几毫秒内完成。

这个时间限制主要是为了防止编程错误而造成得无限循环。

最大执行时间得长短由redis.conf得lua-time-limit控制(单位是毫秒)。也可以使用以下命令去修改:

config get parameter // 获取配置参数
config set parameter value // 修改配置参数

当脚本达到最大执行时间得时候,并不会被redis结束,因为redis必须保证脚本执行得原子性,而中途停止脚本得运行意味着可能会留下未处理完得数据在数据集data set里面

当达到最大执行时间后,会执行以下操作

1. redis记录一个脚本正在超时运行
2. redis开始重新接受其他客户端得命令请求,但是只有script kill和shutdown nosave两个命令会被处理,对于其他请求,redis只是简单返回busy错误。
3. 可以使用一个script kill将一个仅执行只读命令得脚本kill,因为只读并不修改数据,kill后不会破坏数据得完整性
4. 如果脚本已经执行过写命令,那么唯一允许得操作是:shutdown nosave,通过停止服务器来阻止当前数据集写入磁盘。

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

在流水线请求的上希望中使用evalsha命令时,要小心,因为在流水线中必须保证命令的执行顺序。

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

为了防止这个问题,客户端应该实现以下的其中一种措施:

1.总是在流水线中使用eval命令
2.检查流水线中要用到的所有命令,找到其中的eval命令,并使用script exists sha1[sha1...]命令检查要用到的脚本是不是全都已经保存在缓存里面了。如果所需的全部脚本都可以在缓存里找到,那么可以放心的将eval命令改成evalsha命令,否则就要在流水线的顶端将缺少的脚本用script load script命令加上去

script load 

语法

// 将script添加到脚本缓存,但不立即执行。这个执行以后的返回值是给定的script的sha1校验和
script load script

如果给定的脚本已经在缓存里面存在了,那么不做动作

在脚本被加入到缓存后,通过evalsha命令,可以使用脚本的sha1校验和来调用这个脚本。

// 将script添加到脚本缓存,并立即执行
eval script numksys key[key ...] arg[arg ...]

script exists

语法

script exists sha1 [sha1 ...]

给定一个或多个脚本的sha1校验和,返回一个包含0(脚本在缓存中不存在)和1(脚本在焕春中存在)的列表,表示校验和所指定的脚本是否已经被保存在缓存中。

script flush

语法

script flush // 清楚所有Lua脚本缓存

script kill

语法

script kill // 当前正在执行的脚本被kill。执行成功返回OK,否则返回一个错误信息

执行这个脚本的客户端会从eval script numkeys key [key...] arg[arg ...]命令的阻塞当中退出,并收到一个错误作为返回值

如果当前正在运行的脚本已经执行过写操作,那么即使kill,也无法kill掉,因为这是违反lua脚本原子性执行原则的。这种情况下,唯一可行的办法是使用shutdown nosave,通过停止整个redis进程来停止脚本的运行,并防止不完整(half-written)的信息被写入数据库中。

你可能感兴趣的:(redis,lua,数据库)