Redis 中使用服务端 Lua 脚本

Redis 自 2.6 版本起加入了服务端的 Lua 脚本支持,即增添了 EVAL, EVALSHA, SCRIPT 相关命令。Lua 为何物,Lua 是一个非常轻量级,强大,高效,可内嵌的脚本语言; 产自于巴西,源码和二进制包都只有 200 多 KB。当前版本的 Redis 5.0.5 中 Lua 引擎版本是 Lua 5.1(自 Redis 2.6 起就没变,当前 Lua 为 5.3.5),可用 Redis 命令 eval "return _VERSION" 0 查看到。

本文就要探究一下如何在 Redis 中使用 Lua 脚本,以及如何简化与 Redis 的交互。比如说在 Redis 中要先获一个值,然后根据这个值再去 Redis 中获得另一个相关联的值,如果不使用 Lua 脚本就会有两次与 Redis 交互,引入 Lua 脚本可以只用一次操作。

本文不具体讲述 Lua 语言本身,只涉及到与 Redis 相关的 Lua 特性。现在来体验下 Lua 中嵌入 Lua 脚本的基本操作。

Redis Lua 脚本 Hello World

EVAL 命令用来执行 Lua 脚本,返回值直接作为 Redis 的返回值,上面最后那个 0 表示没有 KEY 值。EVAL 的完整命令如下

eval script numKeys key [key ...] arg [arg ...]

script 为 Lua 脚本,numKeys 指明后面有多少个 KEY,一个 KEY 都没有就写成 0, 数完了 KEY 之后的就是一个个的 ARGV (参数) 了。如何在 Lua 脚本中引用 KEY 和 ARGV 看下面的例子

KEYS 和 ARGV 数组(Lua 中叫做 Table)下标是基于 1 开始的,同时见识一下 Lua 拼接字符串是用 .. 双点,{} 括起来的是 Lua 的数组类型表示法。

KEYS 和 ARGV  的区别,从编程上好像没什么不同,但从约定上 KEYS 对应于 Redis 的 key,而 ARGV 可以为其他任何意义的参数。

Lua 中使用 Redis 命令

上面了解了在 Redis 中使用 Lua 脚本的基本用法,现在开始熔入 redis 操作。需要用到 redis.call() 来直接操作 Redis 的命令,该 Redis 命令要用到的参数可由变量, KEYS 或 ARGV 来提供

更复杂一些

一次 Redis  在已知 key1 的情况下辗转由 key1 -> key2 -> hello 获得最终想要的值。

Lua 中可以使用任何 Redis 命令,只要注意变量类型在 Lua 与 Redis 之间的传递与匹配。

浮点类型与 Lua 数组截断

Lua 的浮点类型返回给 Redis  会被转换为整形,Lua 的数组也会被 Redis 从 nil 处截断,如下例子

浮点数只保留整数部分,Lua 数组中 nil 后面的值被忽略掉,因为 Lua 的数组特性就是这样的,从第一个直到非 nil 元素。解决办法可用字符串类型返回浮点数,nil 值替换为空字符串 ''

不过在 Lua 在执行 mget 碰到不存在的 key 时是能够返回 nil 值的

这应该能启发我们另一种方式来用 Lua 返回中间包含 nil 值的数组,有待进一步研究。

Lua 脚本的多种执行方式

前面都是进入到 Redis 交互界面然后用 eval 命令执行 Lua 脚本,接下来尝试多种不同的方式。Redis 中的 Lua 脚本是会预编译的,如果多次用 eval  命令执行的是相同的脚本,只会在第一次执行前编译好,其后相同的脚本直接执行编译好的代码。我们可以用 script load 来加载相同内容的脚本,会发现多次加载会返回一样的 hash 值

相同的脚本内容产生一样的 hash 值,所以只要后面相同的脚本内容执行时无需重新编译。

这个 SHA1 值的是通过调用 redis.sha1hex() 函数产生的,见下面那种方式,只要字符串一样生成的 SHA1 值也是一样的。

频繁的 eval 相同的 Lua 代码块会增加一些网络传输量,特别是对于一大段较复杂的 Lua 脚本,这时候就最好显式的用 SCRIPT LOAD 加载,并用 EVALSHA 来调用它。

加载的脚本会用一个 hash 与其关联,以后使用时就用 EVALSHA 命令指定 hash 值来调用,KEY 或 ARGV 的传入方式是完全一样的。上面相当于执行时翻译为

关于 script load 产生的 hash 值

script load 说是会为脚本内容产生 SHA1 对应的 hash 值,我们可以做一个有意思的测试,进一步验证脚本是预编译的。

知道了相同的脚本文件内容总是产生相同的 hash 值后,那么我们每次在程序启动的时候多次加载相同的脚本都无害的,得到相应脚本的 hash 值其他就直接使用该 hash 值来调用 Redis 中的 Lua 脚本,省却了每一次的脚本传输。

用 redis-cli + Redis 命令执行 Lua 脚本

redis-cli shell 命令后能够直接使用 Redis 的命令,所以下面的操作与在 redis-cli 交互界面操作方式没有分别

加载 Lua 脚本文件

如果 Lua 脚本比较复杂,最好是写在一个单独的 lua 脚本文件, 比如 lua 脚本文件名是 test.lua, 内容如下

我们就可以用以下命令来加载或执行

"$(cat test.lua)" 的作用是读入 test.lua 脚本的内容放置在外部命令外,test.lua 中有双引号也能正确处理。

真正 redis-cli --eval 执行 Lua 脚本

前面两种方式实质还是进到 redis-cli shell 再操作的,这里来看 redis-cli --eval  参数的操作,把 test.lua 的内容改一改更能体现 redis-cli --eval 的差异性,新的内容如下

然后执行 redis-cli --eval 命令

注意到 KEYS 和 ARGV 的传递方式有变,KEYS 之前不再需要用数字告诉脚本有多少个 KEY, 而是用逗号分隔 KEYS 与 ARGV 两个群组。而且得特别留意在 key2, 之间一定要有空格,否则 key2, 会作为一个整体,未发现单独的逗号的话,key1 key2, arg1 arg2 将分别作为四个 KEY。

从 Redis 3.2 起,我们可以用 redis-cli --ldb --eval test.lua key1 key2 , arg1 arg2 对 Lua 脚本进行调试,详情请见 Redis Lua scripts debugger

Redis 中的 Lua 脚本也不能做太耗时的操作,默认最多 5 秒(lua-time-limit 配置),如果 5 秒中未能返回则会告诉客户端 Redis is busy running a script, 由客户端来决定用 script kill 来杀掉该 busy  的脚本。

Java Jedis 对 Lua 的支持

我们在用 Jedis 库连接 Redis 服务时,Jedis 也提供了对 Lua 的支持,以下是 Jedis 中 eval 和 script 两组 API

此处还是来体验一下当传入一批 key 时,Lua 中怎么用 mget 命令来处理,显然我们不该用 KEYS[1], KEYS[2], ... KEYS[n] 来接收,在 Lua 在对 KEYS 循环逐个 get 的话性能上是个问题。

得到执行结果

[v1, null, v2, null]

有了 Lua 的 unpack 函数能够进行如下转换

Java 的 Varargs -> Lua Array -> Redis 的  Varargs

关于执行 Lua 脚本的权限

Redis 集群中分 primary 和 replica  节点类型,replica 是只读的,但是在 replica 上也可以执行 script load 命令加载脚本得到一个 hash, 并执行 evalsha <hash>。只是在 replica 上加载的脚本内部不能执行写入操作,例如 :redis.call('set', 'a', 'b')。还有一点要注意,replica 上加载的脚本不会同步到其他节点上,而在 primary 上加载的脚本可以任意节点上使用相同的 hash 执行。

链接:

  1. Redis Lua scripts debugger
  2. A Speed Guide to Redis Lua Scripting
  3. Lua 教程
  4. Redis 设计与实现 - Lua 脚本
  5. Lua: A Guide for Redis Users

 

本文链接 https://yanbin.blog/redis-builtin-lua-script/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

1 Comment
Inline Feedbacks
View all comments
trackback

[…] Redis 不支持事物回滚   Lua 脚本,见我之前博文 Redis 中使用服务端 Lua 脚本   Redis Lua 脚本中不允许创建全局变量,eval "x=10" 0 […]