Lua 是一个小巧的脚本语言。它是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个由 Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo 三人组成的研究小组于 1993 年开发的。其设计目的是为了通过灵活嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Lua 由标准 C 编写而成,并以源代码形式开放,代码简洁优美,几乎在所有操作系统和平台上都可以编译,运行,可以很容易集成在一些软件系统里,可以为一些中间件提供支持功能,比如 nginx,redis。Lua 并没有提供强大的库,这是由它的定位决定的。所以 Lua 不适合开发独立应用程序。Lua 有一个同时进行的 JIT 项目,提供在特定平台上的即时编译功能。
Lua 脚本可以很容易地被 C/C++ 代码调用,也可以反过来调用 C/C++ 的函数,这使得 Lua 在应用程序中可以被广泛应用。不仅仅作为扩展脚本,也可以作为普通的配置文件,代替 XML,ini 等文件格式,并且更容易理解和维护。 一个完整的 Lua 解释器不过 200k,在所有脚本引擎中,Lua 的速度是最快的。这一切都决定了 Lua 是作为嵌入式脚本的最佳选择。
摘自百度百科:https://baike.baidu.com/item/lua/7570719
Lua 官网:http://www.lua.org/
Lua 官方文档:http://www.lua.org/manual/5.3/
Lua 安装手册:https://www.php.cn/lua/lua-environment.html
Redis 是高性能的 key-value 数据库,在很大程度上克服了 memcached 这类 key/value 存储的不足,在部分场景下,是对关系数据库的良好补充。得益于超高性能和丰富的数据结构,Redis 已成为当前架构设计中的首选 key-value 存储系统。
Redis 提供了非常丰富的指令集,官网上提供了 200 多个命令。但在某些特定领域,需要将业务拆分为若干指令并原子地执行,仅使用原生命令便无法原子性地完成。以 compare and swap 场景为例,如果使用 Redis 原生命令,需要从 Redis 中获取这个 key,然后提取其中的值进行比对,如果相等就更新,如果不相等则不处理。仅仅一个 compare and swap 操作就需要与 Redis 通讯两次,且无法保证原子性。
Redis 问世之后,其开发者也意识到了上述问题,因此 Redis 从 2.6 版本开始支持 Lua 脚本。新版本的 Redis 还支持 Lua Script debug。用户可以向服务器发送 Lua 脚本来执行自定义命令,获取脚本的响应数据。Redis 服务器会单线程原子性执行 Lua 脚本,保证 Lua 脚本在处理过程中不会被任意其它请求打断。
有了 Lua 脚本之后,使用 Redis 程序时便能够在以下方面实现显著提升:
Redis 中使用 Lua 脚本,主要是如下几个命令。
官网对几个命令的介绍:http://www.redis.cn/commands/eval.html
EVAL 和 EVALSHA 命令是从 Redis 2.6.0 版本开始的,使用内置的 Lua 解释器,可以对 Lua 脚本进行求值。
EVAL 的命令格式为:EVAL script numkeys key [key ...] arg [arg ...]
参数说明:
示例如下。
# 使用 redis.call() 去调用 redis 的命令,设置 testkey1 的值为 testvalue1
127.0.0.1:6379> eval "return redis.call('set', KEYS[1], ARGV[1])" 1 testkey1 testvalue1
OK
# 通过 redis.call() 调用 redis 的 get 命令查询 testkey1 的值
127.0.0.1:6379> eval "return redis.call('get', KEYS[1])" 1 testkey1
"testvalue1"
# 将 testkey 值设置为 testvalue,并设置该键的过期时间是 15 秒
127.0.0.1:6379> setex testkey 15 testvalue
OK
# TTL 命令以秒为单位返回 key 的剩余过期时间。
# 当 key 不存在(或已过期)时,返回 -2;当 key 存在但没有设置剩余生存时间时,返回 -1 。
# 否则以秒为单位返回 key 的剩余生存时间。
127.0.0.1:6379> ttl testkey
(integer) 7
127.0.0.1:6379> ttl testkey
(integer) -2
127.0.0.1:6379> get testkey
(nil)
# 通过 redis.call() 调用 redis 的 setex 命令
127.0.0.1:6379> eval "return redis.call('setex', KEYS[1], ARGV[1], ARGV[2])" 1 testkey2 60 testvalue2
OK
上述第一条语句中,script 即为 “return redis.call(‘set’, KEYS[1], ARGV[1])”;numkeys 为 1;key [key …] 为 testkey1;arg [arg …] 为 testvalue1。
在 Lua 脚本中,可以使用两个不同的函数来执行 Redis 命令,它们分别是: redis.call() 和 redis.pcall()。这两个函数很类似,它们唯一的区别在于它们使用不同的方式处理执行命令所产生的错误。当 Redis 命令执行结果返回错误时, redis.call() 脚本会停止执行,返回给调用者一个错误,错误的输出信息会说明错误造成的原因。而 redis.pcall() 出错时并不引发错误,会将捕获的错误以 Lua 表的形式返回。redis.call() 和 redis.pcall() 两个函数的参数可以是任意的 Redis 命令。
命令格式: EVALSHA sha1 numkeys key [key ...] arg [arg ...]
。
命令用途:根据给定的 SHA1 校验和,对缓存在服务器中的脚本进行求值。这个命令的其它地方,比如参数的传入方式,都和 EVAL
命令一样。
将脚本缓存到服务器的操作可以通过 SCRIPT LOAD
命令进行。
Redis 保证所有被运行过的脚本都会被永久保存在脚本缓存当中,这意味着当 EVAL 命令在一个 Redis 实例上成功执行某个脚本之后,随后针对这个脚本的所有 EVALSHA 命令都会成功执行。
刷新脚本缓存的唯一办法是显式地调用 SCRIPT FLUSH
命令,这个命令会清空运行过的所有脚本的缓存。
缓存可以长时间储存而不产生内存问题的原因是,它们的体积非常小,而且数量也非常少,即使脚本在概念上类似于实现一个新命令,即使在一个大规模的程序里有成百上千的脚本,即使这些脚本会经常修改,即便如此,储存这些脚本的内存仍然是微不足道的。
事实上,用户会发现 Redis 不移除缓存中的脚本实际上是一个好主意。比如说,对于一个和 Redis 保持持久化连接的程序来说,它可以确信,执行过一次的脚本会一直保留在内存当中,因此它可以在流水线中使用 EVALSHA
命令而不必担心因为找不到所需的脚本而产生错误。
Redis 提供了以下几个 SCRIPT 命令,用于对脚本子系统进行控制。
EVALSHA
命令,可以使用脚本的 SHA1 校验和来调用这个脚本。 EVAL 命令也会将脚本添加到脚本缓存中,但是它会立即对输入的脚本进行求值。SCRIPT FLUSH
为止。如果给定的脚本已经在缓存里面了,那么不做动作。SCRIPT LOAD
命令将 Lua 脚本命令缓存,生成并返回一个固定长度的 hash 字符串(即 SHA1 校验和),不管你的 Lua 脚本命令有多长,都会返回一个定长的 hash 字符串。这样做的好处是减少网络的传输,因为 Redis 客户端向服务器传输大段的 Lua 脚本命令的时候,会增加网络开销,而将 Lua 命令缓存在 Redis 中,客户端只需要传递这个固定长度的 hash 字符串即可,可以减少传输的消耗,同时脚本缓存在 Redis 中也可以防止脚本被篡改。SHUTDOWN NOSAVE
命令,通过停止整个 Redis 进程来停止脚本的运行,并防止不完整(half-written)的信息被写入数据库中。SCRIPT DEBUG YES | SYNC | NO
。示例如下。
# 缓存脚本,返回 SHA1 校验和
127.0.0.1:6379> script load "return redis.call('set', KEYS[1], ARGV[1])"
"55b22c0d0cedf3866879ce7c854970626dcef0c3"
# 检查指定脚本是否存在,1:存在;0:不存在
127.0.0.1:6379> script exists 55b22c0d0cedf3866879ce7c854970626dcef0c3
1) (integer) 1
127.0.0.1:6379> script exists 55b22c0d0cedf3866879ce7c854970626dcef0c4
1) (integer) 0
127.0.0.1:6379> script exists 55b22c0d0cedf3866879ce7c854970626dcef0c3 55b22c0d0cedf3866879ce7c854970626dcef0c4
1) (integer) 1
2) (integer) 0
# 根据 SHA1 校验和选取指定的缓存脚本执行命令
127.0.0.1:6379> evalsha 55b22c0d0cedf3866879ce7c854970626dcef0c3 1 testkey7 testvalue7
OK
# 清空脚本缓存
127.0.0.1:6379> script flush
OK
127.0.0.1:6379> script exists 55b22c0d0cedf3866879ce7c854970626dcef0c3
1) (integer) 0
127.0.0.1:6379> evalsha 55b22c0d0cedf3866879ce7c854970626dcef0c3 1 testkey8 testvalue8
(error) NOSCRIPT No matching script. Please use EVAL.
# 杀掉运行的脚本
127.0.0.1:6379> script kill
(error) NOTBUSY No scripts in execution right now.
Redis 也可以执行服务器上的 Lua 文件。
if redis.call('get', KEYS[1]) == ARGV[1]
then
return redis.call('setex', KEYS[1], ARGV[2], ARGV[3])
else
return 0
end
上述脚本文件的功能:如果 KEYS[1] 的值与 ARGV[1] 相同,则将 KEYS[1] 的值设为ARGV[3],将过期时间设置为 ARGV[2],并返回 1,否则返回 0,这是一个 CAS 操作。
将文件命名为 compareAndSwap.lua,放到 /home/testuser/ 路径下。
执行命令: redis-cli -a 密码 --eval Lua 脚本路径 key [key ...] , arg [arg ...]
。
脚本路径后紧跟 key [key …] ,相比命令行模式,少了 numkeys 这个 key 的数量值。key [key …] 和 arg [arg …] 之间的英文逗号前后必须有空格,否则报错。
[testuser@vm-10-211-42-26 ~]$ redis-cli -a 123456 --eval /home/testuser/compareAndSwap.lua testkey1 , value1 120 value2
OK
[testuser@vm-10-211-42-26 ~]$ redis-cli -a 123456 --eval /home/testuser/compareAndSwap.lua testkey1 , value2 120 value3
OK
[testuser@vm-10-211-42-26 ~]$ redis-cli -a 123456 --eval /home/testuser/compareAndSwap.lua testkey1 , value2 120 value3
0