Redis Lua 脚本

Redis 使用 Lua 的好处

Lua 简介就不复制了.

Redis 提供了很是富的命令集, 可是用户依然不知足, 但愿能够自定义扩充若干指令来完成一些特定领域的问题.redis

Redis 为这样的用户场景提供了 lua 脚本支持, 用户能够向服务器发送 lua 脚原本执行自定义动做, 获取脚本的响应数据. Redis 服务器会单线程原子性执行 lua 脚本, 保证 lua 脚本在处理的过程当中不会被任意其它请求打断.数据库

对于这点很是像事务, 事务是须要先将命令发给一条一条的发送给 Redis, 而后调用 EXEC 执行事务. 而 Lua 脚本, 能够在服务端直接执行, 因此相应的也减小了网络带宽.编程

clipboard.png

Redis 中 Lua 脚本相关命令

SCRIPT LOAD 命令

SCRIPT LOAD script

脚本 script 添加到脚本缓存中, 但并不当即执行这个脚本.数组

返回给定 script 的 SHA1 校验和.缓存

一个最简单的例子:服务器

127.0.0.1:6379> SCRIPT LOAD "return 'hello moto'"
"232fd51614574cf0867b83d384a5e898cfd24e5a"
127.0.0.1:6379> EVALSHA 232fd51614574cf0867b83d384a5e898cfd24e5a 0
"hello moto"
值得注意的是:
若是给定的脚本已经在缓存里面了, 那么不作动做.
脚本能够在缓存中保留无限长的时间, 直到执行 SCRIPT FLUSH 为止.

SCRIPT FLUSH 命令

SCRIPT FLUSH

这个命令没啥好说的, 就是清除全部 Lua 脚本缓存.网络

返回值老是返回 OK函数

EVALSHA 命令

EVALSHA sha1 numkeys key [key ...] arg [arg ...]

根据给定的 sha1 校验码, 对缓存在服务器中的脚本进行执行.工具

EVAL 命令

EVAL script numkeys key [key …] arg [arg …]

script 参数是一段 Lua 5.1 脚本程序, 它会被运行在 Redis 服务器上下文中.lua

numkeys 参数用于指定键名参数的个数.

键名参数 key [key ...]EVAL 的第三个参数开始算起, 表示在脚本中所用到的哪些 Redis 键(key), 这些键名参数能够在 Lua 中经过全局变量 KEYS 数组, 用 1 为基址的形式访问 ( KEYS[1], KEYS[2], 以此类推).

在命令的最后, 附加参数 arg [arg ...], 能够在 Lua 中经过全局变量 ARGV 数组访问, 访问的形式和 KEYS 变量相似( ARGV[1]ARGV[2], 诸如此类).

上面这几段长长的说明能够用一个简单的例子来归纳:

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

其中 "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 是被求值的 Lua 脚本, 数字 2 指定了键名参数的数量, key1key2 是键名参数, 分别使用 KEYS[1]KEYS[2] 访问, 而最后的 firstsecond 则是附加参数, 能够经过 ARGV[1]ARGV[2] 访问它们.

键名参数 能够理解为, 脚本可能读取或写入的键.
附加参数 能够理解为, 逻辑判断条件或要写入的数据.

在 Lua 脚本中执行 Redis 命令.

在 Lua 脚本中, 可使用两个不一样函数来执行 Redis 命令, 它们分别是:

  • redis.call()
  • redis.pcall()

好比下面的这段脚本:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

使用 EVAL 命令执行:

127.0.0.1:6379> set foo bar
OK
127.0.0.1:6379> eval 'if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end' 1 foo bar
(integer) 1

SCRIPT EXISTS 命令

SCRIPT EXISTS sha1 [sha1 …]

判断一个或多个脚本的 SHA1 校验和, 是否已经添加到脚本缓存中.

存在返回1, 不存在返回0.

127.0.0.1:6379> SCRIPT EXISTS 232fd51614574cf0867b83d384a5e898cfd24e5a 232fd51614574cf0867b83d384a5e898cfd24e5b
1) (integer) 1
2) (integer) 0

SCRIPT KILL 命令

SCRIPT KILL

杀死当前正在运行的 Lua 脚本, 只有这个脚本没有执行过任何写操做时, 这个命令才生效.

SCRIPT KILL 执行以后, 当前正在运行的脚本会被杀死, 执行这个脚本的客户端会从 EVAL script numkeys key [key …] arg [arg …] 命令的阻塞当中退出, 并收到一个错误做为返回值.

另外一方面, 假如当前正在运行的脚本已经执行过写操做, 那么即便执行 SCRIPT KILL, 也没法将它杀死, 由于这是违反 Lua 脚本的原子性执行原则的.

在这种状况下, 惟一可行的办法是使用 SHUTDOWN NOSAVE 命令, 经过中止整个 Redis 进程来中止脚本的运行, 并防止不完整 (half-written) 的信息被写入数据库中.

执行成功返回 OK, 不然返回一个错误.

# 没有脚本在执行时

redis> SCRIPT KILL
(error) ERR No scripts in execution right now.

# 成功杀死脚本时

redis> SCRIPT KILL
OK
(1.30s)

# 尝试杀死一个已经执行过写操做的脚本,失败

redis> SCRIPT KILL
(error) ERR Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in an hard way using the SHUTDOWN NOSAVE command.
(1.69s)

如下是脚本被杀死以后, 返回给执行脚本的客户端的错误:

redis> EVAL "while true do end" 0
(error) ERR Error running script (call to f_694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7): Script killed by user with SCRIPT KILL...
(5.00s)

错误处理

redis.call()redis.pcall() 的惟一区别在于它们对错误处理的不一样.

redis.call() 在执行命令的过程当中发生错误时, 脚本会中止执行, 并返回一个脚本错误, 错误的输出信息会说明错误形成的缘由:

redis> lpush foo a
(integer) 1

redis> eval "return redis.call('get', 'foo')" 0
(error) ERR Error running script (call to f_282297a0228f48cd3fc6a55de6316f31422f5d17): ERR Operation against a key holding the wrong kind of value

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

redis 127.0.0.1:6379> EVAL "return redis.pcall('get', 'foo')" 0
(error) ERR Operation against a key holding the wrong kind of value

带宽 和 EVALSHA

EVAL 命令要求你在每次执行脚本的时候都发送一次脚本主体(script body).

Redis 有一个内部的缓存机制, 所以它不会每次都从新编译脚本, 不过在不少场合, 付出无谓的带宽来传送脚本主体并非最佳选择.

为了减小带宽的消耗, Redis 实现了 EVALSHA 命令, 它的做用和 EVAL 同样, 都用于对脚本求值, 但它接受的第一个参数不是脚本, 而是脚本的 SHA1 校验和.

EVALSHA 命令的表现以下:

  • 若是服务器还记得给定的 SHA1 校验和所指定的脚本, 那么执行这个脚本.
  • 若是服务器不记得给定的 SHA1 校验和所指定的脚本, 那么它返回一个特殊的错误, 提醒用户使用 EVAL 代替 EVALSHA.

如下是示例:

> set foo bar
OK

> eval "return redis.call('get','foo')" 0
"bar"

> evalsha 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 0
"bar"

> evalsha ffffffffffffffffffffffffffffffffffffffff 0
(error) `NOSCRIPT` No matching script. Please use [EVAL](/commands/eval).
值得注意的是:
可使用 EVALSHA 来代替 EVAL, 当出现 NOSCRIPT 错误时, 才使用 EVAL 命令从新发送脚本, 这样就能够最大限度地节省带宽.

执行 EVAL 命令时, 要使用正确的格式来传递键名参数和附加参数, 由于若是将参数硬写在脚本中, 那么每次当参数改变的时候, 都要从新发送脚本, 即便脚本的主体并无改变.
相反, 经过使用正确的格式来传递键名参数和附加参数, 就能够在脚本主体不变的状况下, 直接使用 EVALSHA 命令对脚本进行复用, 免去了无谓的带宽消耗.

全局变量保护

为了防止没必要要的数据泄漏进 Lua 环境, Redis 脚本不容许建立全局变量. 若是一个脚本须要在屡次执行之间维持某种状态, 它应该使用 Redis key 来进行状态保存.

企图在脚本中访问一个全局变量(不论这个变量是否存在)将引发脚本中止, EVAL 命令会返回一个错误:

redis 127.0.0.1:6379> eval 'a=10' 0
(error) ERR Error running script (call to f_933044db579a2f8fd45d8065f04a8d0249383e57): user_script:1: Script attempted to create global variable 'a'

Lua 的 debug 工具, 或者其余设施, 好比打印 (alter) 用于实现全局保护的 meta table, 均可以用于实现全局变量保护.

一旦用户在脚本中混入了 Lua 全局状态, 那么 AOF 持久化和复制 (replication) 都会没法保证, 因此, 请不要使用全局变量.

避免引入全局变量的一个诀窍是: 将脚本中用到的全部变量都使用 local 关键字定义为局部变量.

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

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

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

最大执行时间的长短由 lua-time-limit 选项来控制(以毫秒为单位), 能够经过编辑 redis.conf 文件或者使用 CONFIG GET parameterCONFIG SET parameter value 命令来修改它.

当一个脚本达到最大执行时间的时候, 它并不会自动被 Redis 结束, 由于 Redis 必须保证脚本执行的原子性, 而中途中止脚本的运行意味着可能会留下未处理完的数据在数据集里面.

所以, 当脚本运行的时间超过最大执行时间后, 如下动做会被执行:

  • Redis 记录一个脚本正在超时运行.
  • Redis 开始从新接受其余客户端的命令请求, 可是只有 SCRIPT KILLSHUTDOWN NOSAVE 两个命令会被处理, 对于其余命令请求, Redis 服务器只是简单地返回 BUSY 错误.
  • 可使用 SCRIPT KILL 命令将一个仅执行只读命令的脚本杀死, 由于只读命令并不修改数据, 所以杀死这个脚本并不破坏数据的完整性.
  • 若是脚本已经执行过写命令, 那么惟一容许执行的操做就是 SHUTDOWN NOSAVE, 它经过中止服务器来阻止当前数据集写入磁盘.

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

在流水线请求的上下文中使用 EVALSHA 命令时, 要特别当心, 由于在流水线中, 必须保证命令的执行顺序.

一旦在流水线中由于 EVALSHA 命令而发生 NOSCRIPT 错误, 那么这个流水线就再也没有办法从新执行了, 不然的话, 命令的执行顺序就会被打乱.

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

  • 老是在流水线中使用 EVAL 命令.
  • 检查流水线中要用到的全部命令, 找到其中的 EVAL 命令, 并使用 SCRIPT EXISTS sha1 [sha1 …] 命令检查要用到的脚本是否是全都已经保存在缓存里面了. 若是所需的所有脚本均可以在缓存里找到, 那么就能够放心地将全部 EVAL 命令改为 EVALSHA 命令, 不然的话, 就要在流水线的顶端 (top) 将缺乏的脚本用 SCRIPT LOAD script 命令加上去.
相关文章
相关标签/搜索