Redis学习笔记 - Lua脚本(1) - 使用Lua脚本

樊宏义
2023-12-01

参考:

Redis从2.6.0版本开始支持Lua脚本,通过在服务器嵌入Lua环境,Redis客户端可以使用Lua脚本,直接在服务器端原子地执行多个Redis命令。

1.使用Lua脚本的好处:

  • 减少网络开销:可以将多个请求通过脚本的形式一次发送,减少网络时延。
  • 原子操作:redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。
  • 复用:客户端发送的脚本会永久存在redis中,这样,其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑。

2.相关命令:

  • EVAL:执行 Lua 脚本
  • EVALSHA:根据给定的 sha1 校验码,执行 Lua 脚本
  • SCRIPT DEBUG:使用EVAL可以开启对脚本的调试(3.2.0版本)
  • SCRIPT EXISTS:查看指定的脚本是否已经被保存在缓存当中
  • SCRIPT FLUSH:从脚本缓存中移除所有脚本
  • SCRIPT KILL:杀死当前正在运行的 Lua 脚本
  • SCRIPT LOAD:将脚本 script 添加到脚本缓存中,但并不立即执行这个脚本

一、EVAL

EVAL script numkeys key [key ...] arg [arg ...]

  • script:参数是一段 Lua 5.1 脚本程序。脚本不必(也不应该)定义为一个 Lua 函数
  • numbers:指定键名参数的个数
  • key:表示在脚本中用到的Redis 键(key),这些键名参数可以在Lua中通过全局变量KEYS数组访问,下标从1开始,如: KEYS[1]、KEYS[2]
  • arg:参数,在Lua中通过全局变量ARGV数组访问,下标从1开始,如:ARGV[1]、ARGV[2]

示例:

(1)使用KEYS数组、ARGV数组

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

(2)输出list中所有元素

redis> rpush nums 1 2 3
(integer) 3

redis> eval "return redis.call('lrange', KEYS[1], 0, -1)" 1 nums
1) "1"
2) "2"
3) "3"

(3)集合去重

使用lua脚本文件方式,在命令行里执行,和在redis里执行不太一样,命令如下:
redis-cli --eval lua_file key1 key2 , arg1 arg2 arg3

示例:

  • 插入测试数据
redis> sadd nums 1 2 3
(integer) 3

redis> smembers nums
1) "1"
2) "2"
3) "3"
  • member.lua脚本
-- key
local key = KEYS[1]

-- 所有参数
local args = ARGV

-- 初始化result
local result = {}

-- 判断元素是否存在
for m, n in ipairs(args) do
    local is_repeat = redis.call("sismember", key, n)
    if (is_repeat) then
        table.insert(result, 1, n)
    end
end
return result
  • 测试
$ redis-cli --eval ./member.lua nums , 1 2
1) "2"
2) "1"

二、SCRIPT LOAD、EVALSHA

  • SCRIPT LOAD script:将脚本 script 添加到脚本缓存中,但并不立即执行这个脚本
    • script:lua脚本
  • EVALSHA sha1 numkeys key [key ...] arg [arg ...]:根据给定的 sha1 校验码,执行Lua脚本
    • sha1: 通过 SCRIPT LOAD 生成的 sha1 校验码
    • 其他参数同EVAL命令

示例:

redis> script load "return 'hello redis'"
"69dd69fc0ba1e25d8e2972008b6baee8eccf7da6"

redis> evalsha 69dd69fc0ba1e25d8e2972008b6baee8eccf7da6 0
"hello redis"

三、SCRIPT EXISTS、SCRIPT FLUSH

  • SCRIPT EXISTS script [script ...]:查看指定的脚本是否已经被保存在缓存当中
    • script:通过 SCRIPT LOAD 生成的 sha1 校验码
  • SCRIPT FLUSH:从脚本缓存中移除所有脚本

示例:

redis> script load "return 'hello redis'"
"69dd69fc0ba1e25d8e2972008b6baee8eccf7da6"

redis> script exists 69dd69fc0ba1e25d8e2972008b6baee8eccf7da6
1) (integer) 1

redis> script flush
OK

redis> script exists 69dd69fc0ba1e25d8e2972008b6baee8eccf7da6
1) (integer) 0

四、SCRIPT KILL

SCRIPT KILL :杀死当前正在运行的 Lua 脚本,当且仅当这个脚本没有执行过任何写操作时,这个命令才生效

注:

  • 这个命令主要用于终止运行时间过长的脚本,比如一个因为 BUG 而发生无限循环的脚本。
  • SCRIPT KILL 执行之后,当前正在运行的脚本会被杀死,执行这个脚本的客户端会从 EVAL 命令的阻塞当中退出,并收到一个错误作为返回值。

示例:

客户端A:

# 当前没有正在执行的脚本
redisA> script kill
(error) NOTBUSY No scripts in execution right now.

# 客户端B执行脚本,客户端A杀死运行的脚本
redis> script kill
OK
(0.92s)

客户端B:

# 脚本死循环
redisB> eval "local a = 1; while 1 do a=1; end" 0

# 被杀死后,返回以下信息
(error) ERR Error running script (call to f_c8600a7388e4dd63490c59ede33602dbef5971eb): @user_script:1: Script killed by user with SCRIPT KILL...
(5.00s)

五、SCRIPT DEBUG

SCRIPT DEBUG YES|SYNC|NO:使用EVAL可以开启对脚本的调试

  • YES:打开非阻塞异步调试模式,调试Lua脚本(回退修改的数据)
  • SYNC:打开阻塞同步调试模式,调试Lua脚本(保留修改的数据)
  • NO:关闭脚本调试模式

注:LDB可以设置成两种模式:同步和异步。

  • 异步模式下,服务器会创建新的调试连接,不阻塞其他连接,同时在调试连接结束后会回滚所有的数据修改,这可以保证再次调试时初始状态不变。
  • 同步模式下,调试过程中,服务器其他连接会被阻塞,当调试结束后,所有的数据修改会被保存。

示例:

(1)redis内调试

# 异步调试,回退修改后的数据
redis> script debug yes
OK

# 测试脚本
redis> eval "local a = 1; \n local b = 2; \n return 3" 0
* Stopped at 1, stop reason = step over
-> 1   local a = 1;

# 使用 n 单步调试
redis> n
* Stopped at 2, stop reason = step over
-> 2    local b = 2;

redis> n
* Stopped at 3, stop reason = step over
-> 3    return 3

redis> n
(integer) 3

(Lua debugging session ended -- dataset changes rolled back)

(2)命令行内调试

使用第一节中的示例(3),命令中添加--ldb进入调试模式:redis-cli --ldb --eval ./member.lua nums , 1 2

  • 进入调试模式后,通过 ns 进行单步调试,直到结束
  • 默认是异步模式,调试结束后回滚修改的数据
  • 使用--ldb-sync-mode进入同步模式,此时服务器其他链接被阻塞,调试结束后修改后的数据会被保存

调试示例:【参考:http://blog.huangz.me/2017/redis-lua-debuger-introduction.html

$ redis-cli --ldb  --eval ./member.lua nums , 1 2
Lua debugging session started, please use:
quit    -- End the session.
restart -- Restart the script in debug mode again.
help    -- Show Lua script debugging commands.

* Stopped at 2, stop reason = step over
-> 2   local key = KEYS[1]

lua debugger> n
* Stopped at 5, stop reason = step over
-> 5   local args = ARGV

lua debugger> n
* Stopped at 8, stop reason = step over
-> 8   local result = {}

lua debugger> n
* Stopped at 11, stop reason = step over
-> 11  for m, n in ipairs(args) do

lua debugger> n
* Stopped at 12, stop reason = step over
-> 12      local is_repeat = redis.call("sismember", key, n)

lua debugger> n
<redis> sismember nums 1
<reply> 1
* Stopped at 13, stop reason = step over
-> 13      if (is_repeat) then

lua debugger> n
* Stopped at 14, stop reason = step over
-> 14          table.insert(result, 1, n)

lua debugger> n
* Stopped at 11, stop reason = step over
-> 11  for m, n in ipairs(args) do

lua debugger> n
* Stopped at 12, stop reason = step over
-> 12      local is_repeat = redis.call("sismember", key, n)

lua debugger> n
<redis> sismember nums 2
<reply> 1
* Stopped at 13, stop reason = step over
-> 13      if (is_repeat) then

lua debugger> n
* Stopped at 14, stop reason = step over
-> 14          table.insert(result, 1, n)

lua debugger> n
* Stopped at 11, stop reason = step over
-> 11  for m, n in ipairs(args) do

lua debugger> n
* Stopped at 17, stop reason = step over
-> 17  return result

lua debugger> n
1) "2"
2) "1"

(Lua debugging session ended -- dataset changes rolled back)
 类似资料: