1.1.1. 23.LUA 脚本执行原理

简介

有些时候,Redis 的指令可能不满足于我们的需要,所以 Redis 提供了 Lua 脚本支持。

Redis 会单线程原子性的执行 lua 脚本,确保执行过程中,不会被其他请求中断。

1

首先,编写好脚本如,

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

然后单行化,执行。

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
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) 0

EVAL 指令的第一个参数是脚本字符串,第二个参数是需要传入参数的数量,然后是 key 串和对应的 value 串

EVAL SCRIPT KEY_NUM KEY1 KEY2 ... KEYN ARG1 ARG2 ....

SCRIPT LOAD 和 EVALSHA 指令

当如果脚本内容很长,并且需要频繁执行的话,每次都需要传冗长的脚本势必会造成网络资源浪费,所以 Redis 提供了 SCRIPT LOAD 和 EVALSHA 指令。

SCRIPT LOAD 会将脚本内容传入服务器,但是不执行,并且得到一个 sha1 算法算出的字符串,这个字符串就是传入脚本的 id。 然后,通过 EVALSHA 指令来反复执行这个脚本。

# 加载脚本
127.0.0.1:6379> script load 'local curVal = redis.call("get", KEYS[1]); if curVal == false then curVal = 0 else curVal = tonumber(curVal) end; curVal = curVal * tonumber(ARGV[1]); redis.call("set", KEYS[1], curVal); return curVal'
"be4f93d8a5379e5e5b768a74e77c8a4eb0434441" # 得到 id

# 执行
127.0.0.1:6379> evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 notexistskey 5
(integer) 0
127.0.0.1:6379> evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 notexistskey 5
(integer) 0
127.0.0.1:6379> set foo 1
OK
127.0.0.1:6379> evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 foo 5
(integer) 5
127.0.0.1:6379> evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 foo 5
(integer) 25

错误处理

上面的脚本必须传整数,如果不是整数则会报错。

127.0.0.1:6379> evalsha be4f93d8a5379e5e5b768a74e77c8a4eb0434441 1 foo bar
(error) ERR Error running script (call to f_be4f93d8a5379e5e5b768a74e77c8a4eb0434441): @user_script:1: user_script:1: attempt to perform arithmetic on a nil value

当 Redis 报错时,前面执行了的 redis.call 产生的影响是无法 rollback 。

lua 的替代方案是内置了 pcall(f) 函数调用。pcall 的意思是 protected call,它会让 f 函数运行在保护模式下,f 如果出现了错误,pcall 调用会返回 false 和错误信息。而普通的 call(f) 调用在遇到错误时只会向上抛出异常。在 Redis 的源码中可以看到 lua 脚本的执行被包裹在 pcall 函数调用中。

错误传递

当使用 call 函数时出错了,只会得到一个通用的错误

127.0.0.1:6379> hset foo x 1 y 2
(integer) 2
127.0.0.1:6379> eval 'return redis.call("incr", "foo")' 0
(error) ERR Error running script (call to f_8727c9c34a61783916ca488b366c475cb3a446cc): @user_script:1: WRONGTYPE Operation against a key holding the wrong kind of value

当我们将 call 换成 pcall 则会给出具体的错误提示。

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

脚本死循环怎么办?

127.0.0.1:6379> eval 'while(true) do print("hello") end' 0

当执行上面的命令后,Redis 会出现明显卡死。打开 redis 服务器日志可以看到疯狂输出 hello 。这个时候需要另开一个 redis-cli 执行 script kill 指令。

127.0.0.1:6379> script kill
OK
(2.58s)

而原窗口则会:

127.0.0.1:6379> eval 'while(true) do print("hello") end' 0
(error) ERR Error running script (call to f_d395649372f578b1a0d3a1dc1b2389717cadf403): @user_script:1: Script killed by user with SCRIPT KILL...
(6.99s)

我们可能会发现有以下问题:

  1. script kill 为什么执行了 2.58 秒。
  2. redis 明明被卡死,怎么可以执行 script kill。
  3. redis-cli 建立链接有点慢,大约顿了 1 秒。

Script Kill 的原理

原理图

lua 脚本提供了各种钩子函数,它允许在内部虚拟机执行指令时运行钩子代码。比如每执行 N 条指令执行一次某个钩子函数。

Redis 在钩子函数里会忙里偷闲处理客户端的请求,并且只有发现 lua 脚本执行超时之后才会去处理请求,这个时间默认是 5s。

Copyright © Kagami丶 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-10 20:25:25

results matching ""

    No results matching ""