1. Lua脚本

    1.1 简介

    1.2 使用lua脚本好处

2. Redis客户端中执行Lua脚本

3. Redis执行lua脚本文件

    3.1 编写Lua脚本文件

    3.2 执行Lua脚本文件 

4. Redis服务器中lua环境

    4.1 创建并修改lua环境

    4.2 lua伪客户端

    4.3 lua_scripts字典


    在redis分布式锁的那篇博客中我们介绍到,为了避免在分布式环境中释放别人的锁,释放锁时需要使用 GET + DEL 两条命令,而为了让着两条命令作为一个原子操作执行,我们可以使用Lua脚本来保证,接下来我们介绍Lua脚本

1. Lua脚本

 1.1 简介

    Lua 是一个小巧的脚本语言。是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组,由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo所组成并于1993年开发。 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Lua由标准C编写而成,几乎在所有操作系统和平台上都可以编译,运行。Lua并没有提供强大的库,这是由它的定位决定的。所以Lua不适合作为开发独立应用程序的语言。Lua 有一个同时进行的JIT项目,提供在特定平台上的即时编译功能。 Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,这使得Lua在应用程序中可以被广泛应用。不仅仅作为扩展脚本,也可以作为普通的配置文件,代替XML,ini等文件格式,并且更容易理解和维护。 Lua由标准C编写而成,代码简洁优美,几乎在所有操作系统和平台上都可以编译,运行。一个完整的Lua解释器不过200k,在目前所有脚本引擎中,Lua的速度是最快的。这一切都决定了Lua是作为嵌入式脚本的最佳选择。

1.2 使用lua脚本好处

(1)减少网络开销:可以将多个请求通过脚本的形式一次发送,减少网络时延和请求次数。

(2)原子性的操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。

(3)代码复用:客户端发送的脚步会永久存在redis中,这样,其他客户端可以复用这一脚本来完成相同的逻辑。

(4)速度快:与其它语言的性能比较, 还有一个 JIT编译器可以显著地提高多数任务的性能; 对于那些仍然对性能不满意的人, 可以把关键部分使用C实现, 然后与其集成, 这样还可以享受其它方面的好处。

(5)可以移植:只要是有ANSI C 编译器的平台都可以编译,你可以看到它可以在几乎所有的平台上运行:从 Windows 到Linux,同样Mac平台也没问题, 再到移动平台、游戏主机,甚至浏览器也可以完美使用 (翻译成JavaScript).

(6)源码小巧:20000行C代码,可以编译进182K的可执行文件,加载快,运行快。

2. Redis客户端中执行Lua脚本

  Redis中lua脚本命令的使用参考Redis常用命令这篇博客。Redis提供的lua脚本的命令包含如下几个,分别是:

(1)- EVAL

    执行lua脚本。

(2)- SCRIPT LOAD 

    将脚本 script 添加到Redis服务器的脚本缓存中,并不立即执行这个脚本,而是会立即对输入的脚本进行求值。并返回给定脚本的 SHA1 校验和。如果给定的脚本已经在缓存里面了,那么不执行任何操作。

(3)- EVALSHA

    考虑到我们通过eval执行lua脚本,脚本比较长的情况下,每次调用脚本都需要把整个脚本传给redis,比较占用带宽。为了解决这个问题,redis提供了EVALSHA命令允许开发者通过脚本内容的SHA1摘要来执行脚本。该命令的用法和EVAL一样,只不过是将脚本内容替换成脚本内容的SHA1摘要。

(4)- SCRIPT EXISTS

    给定一个或多个脚本的 SHA1 校验和,返回一个包含 0 和 1 的列表,表示校验和所指定的脚本是否已经被保存在缓存当中

(5)- SCRIPT FLUSH

    清除Redis服务端所有 Lua 脚本缓存

(6)- SCRIPT KILL

     杀死当前正在运行的 Lua 脚本,当且仅当这个脚本没有执行过任何写操作时,这个命令才生效。 这个命令主要用于终止运行时间过长的脚本,比如一个因为 BUG 而发生无限 loop 的脚本,诸如此类。

    下面简单介绍下EVAL命令的使用。Redis提供了EVAL命令可以使开发者像调用其他Redis内置命令一样调用脚本:

[EVAL] [脚本内容] [key参数的数量] [key …] [arg …]

    可以通过key和arg这两个参数向脚本中传递数据,他们的值可以在脚本中分别使用KEYS和ARGV 这两个类型的全局变量访问。比如我们通过脚本实现一个set命令,通过在redis客户端中调用,那么执行的语句是:

eval "return redis.call('set',KEYS[1],ARGV[1])" 1 hello world

    EVAL命令是根据 key参数的数量-也就是上面例子中的1来将后面所有参数分别存入脚本中KEYS和ARGV两个表类型的全局变量。当脚本不需要任何参数时也不能省略这个参数。如果没有参数则为0:

eval "return redis.call(‘get’,’hello’)" 0

3. Redis执行lua脚本文件

    上一节中介绍的命令,是在redis客户端中使用命令进行操作,接下来我们介绍的是直接执行 Lua 的脚本文件。

3.1 编写Lua脚本文件

local key = KEYS[1]
local val = redis.call("GET", key);

if val == ARGV[1]
then
        redis.call('SET', KEYS[1], ARGV[2])
        return 1
else
        return 0
end

3.2 执行Lua脚本文件 

执行命令: redis-cli -a 密码 --eval Lua脚本路径 key [key …] ,  arg [arg …] 
如:redis-cli -a 123456 --eval ./Redis_CompareAndSet.lua userName , zhangsan lisi 

注意:"--eval"而不是命令模式中的"eval",一定要有前端的两个-,脚本路径后紧跟key [key …],相比命令行模式,少了numkeys这个key数量值。key [key …] 和 arg [arg …] 之间的“ , ”,英文逗号前后必须有空格,否则死活都报错。

## Redis客户端执行
127.0.0.1:6379> set userName zhangsan 
OK
127.0.0.1:6379> get userName
"zhangsan"

## linux服务器执行
## 第一次执行:compareAndSet成功,返回1
## 第二次执行:compareAndSet失败,返回0
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_CompareAndSet.lua userName , zhangsan lisi
(integer) 1
[root@vm01 learn_lua]# redis-cli -a 123456 --eval Redis_CompareAndSet.lua userName , zhangsan lisi
(integer) 0

4. Redis服务器中lua环境

4.1 创建并修改lua环境

    为了在redis服务器中执行Lua脚本,redis服务器内嵌了一个Lua环境,并对这个Lua环境进行了一系列修改,从而确保这个Lua环境可以满足服务器的需要。redis 服务器创建并修改Lua环境的整个过程由以下步骤组成:

(1)创建一个基础的Lua环境,之后的所有修改都是针对这个环境进行的

(2)载入多个函数库到Lua环境里面,让Lua脚本可以使用这些函数库进行数据操作

(3)创建全局表格redis,这个表格包含了对redis进行操作的函数,比如用于在Lua脚本中执行redis命令的redis.call函数

(4)使用redis自制的随机函数替换Lua原有的带有副作用的随机函数,从而避免在脚本中引入副作用

(5)创建排序辅助函数,Lua环境使用这个辅助函数来对一部分Redis命令的结果进行排序,从而消除这些命令的不确定性

(6)创建redis.pcall函数的错误报告辅助函数,这个函数可以提供更详细的出错信息

(7)对Lua环境中的全局环境进行保护,防止用户在执行lua脚本的过程中,将额外的全局变量添加到Lua环境中

(8)将完成修改的Lua环境保存到服务器状态的lua属性中,等待执行服务器传来的lua脚本。

4.2 lua伪客户端

    因为执行redis命令必须要有相应的客户端状态,所以为了执行Lua脚本中包含的redis命令,redis服务器专门为lua环境创建了一个伪客户端,并由这个伪客户端负责处理Lua脚本中包含的所有redis命令。lua脚本使用redis.call或者redis.pcall函数执行一个redis命令,需要完成以下步骤:

(1)Lua环境将redis.call函数或者redis.pcall函数想要执行的命令传给伪客户端

(2)伪客户端将脚本想要执行的命令传给命令执行器

(3)命令执行器执行伪客户端传给它的命令,并将命令的执行结果返回给伪客户端

(4)伪客户端接收命令执行器返回的命令结果,并将这个结果返回给lua环境

(5)lua环境在接收到命令的结果后,将该结果返回给redis.call或者redis.pcall函数

(6)接收到结果的redis.call函数或者redis.pcall函数会将命令结果作为函数返回值返回给脚本中的调用者。

4.3 lua_scripts字典

    除了伪客户端外,redis服务器伪lua环境创建了一个lua_scripts字典,这个字典的键为某个lua脚本的SHA1校验和,而字典的值则是SHA1校验和对应的lua脚本:

struct redisServer {
    // ...

    dict *lua_scripts;
    
    // ...
};

    redis服务器会将所有被EVAL命令执行过的Lua脚本,以及所有被SCRIPT LOAD命令载入过的Lua脚本都保存到这个字典中,这个字典有两个作用,一个时实现SCRIPT EXIST命令,另一个是实现脚本复制功能。

参考:https://zhuanlan.zhihu.com/p/77484377

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐