Redis Lua 脚本常用操作总结及实现 CAS 操作

一、什么是 Lua ?

  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 中为什么引入 Lua 脚本?

  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 程序时便能够在以下方面实现显著提升:

  • 减少网络开销
    本来 N 次网络请求的操作,可以用一个请求完成。原先 N 次请求的逻辑放在 Redis 服务器上完成,减少了网络往返时延。
  • 原子操作
    Redis 会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在脚本运行过程中无需担心会出现竞争条件,无需使用事务。
    Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。 这和使用 MULTI / EXEC 包围的事务很类似。在其他别的客户端看来,脚本的效果要么是不可见的 (not visible),要么就是已完成的 (already completed)。 另一方面,这也意味着,执行一个运行缓慢的脚本并不是一个好主意。写一个跑得很快很顺溜的脚本并不难, 因为脚本的运行开销非常少,但是当你不得不使用一些跑得比较慢的脚本时,请小心, 因为当这些蜗牛脚本在慢吞吞地运行的时候,其他客户端会因为服务器正忙而无法执行命令。
  • 复用
    客户端发送的脚本会永久存储在 Redis 中。这样其他客户端就可以复用这一脚本,而不需要使用代码完成同样的逻辑。
三、Redis 中使用 Lua 脚本

  Redis 中使用 Lua 脚本,主要是如下几个命令。

  • EVAL
  • EVALSHA
  • SCRIPT LOAD
  • SCRIPT EXISTS
  • SCRIPT FLUSH
  • SCRIPT KILL
  • SCRIPT DEBUG

  官网对几个命令的介绍:http://www.redis.cn/commands/eval.html

3.1 EVAL

  EVAL 和 EVALSHA 命令是从 Redis 2.6.0 版本开始的,使用内置的 Lua 解释器,可以对 Lua 脚本进行求值。
  EVAL 的命令格式为:EVAL script numkeys key [key ...] arg [arg ...]

  参数说明:

  • script:该参数是一段 Lua 5.1 脚本程序,是一段 Lua 脚本字符串;这段 Lua 脚本不需要(也不应该)定义函数。它运行在 Redis 服务器中;
  • numkeys:用于指定键名参数的个数;
  • key [key …]:从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 1 为基址的形式访问(KEYS[1],KEYS[2] ,以此类推);
  • arg [arg …]:附加参数,在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似(ARGV[1]、ARGV[2])。

  示例如下。

# 使用 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 命令。

3.2 EVALSHA

  命令格式: EVALSHA sha1 numkeys key [key ...] arg [arg ...]
  命令用途:根据给定的 SHA1 校验和,对缓存在服务器中的脚本进行求值。这个命令的其它地方,比如参数的传入方式,都和 EVAL 命令一样。
  将脚本缓存到服务器的操作可以通过 SCRIPT LOAD 命令进行。

3.3 SCRIPT 命令与脚本缓存

  Redis 保证所有被运行过的脚本都会被永久保存在脚本缓存当中,这意味着当 EVAL 命令在一个 Redis 实例上成功执行某个脚本之后,随后针对这个脚本的所有 EVALSHA 命令都会成功执行。
  刷新脚本缓存的唯一办法是显式地调用 SCRIPT FLUSH 命令,这个命令会清空运行过的所有脚本的缓存。
  缓存可以长时间储存而不产生内存问题的原因是,它们的体积非常小,而且数量也非常少,即使脚本在概念上类似于实现一个新命令,即使在一个大规模的程序里有成百上千的脚本,即使这些脚本会经常修改,即便如此,储存这些脚本的内存仍然是微不足道的。
  事实上,用户会发现 Redis 不移除缓存中的脚本实际上是一个好主意。比如说,对于一个和 Redis 保持持久化连接的程序来说,它可以确信,执行过一次的脚本会一直保留在内存当中,因此它可以在流水线中使用 EVALSHA 命令而不必担心因为找不到所需的脚本而产生错误。

  Redis 提供了以下几个 SCRIPT 命令,用于对脚本子系统进行控制。

  • SCRIPT FLUSH
    清除所有 Lua 脚本缓存
  • SCRIPT EXISTS
    根据给定的脚本校验和,检查指定的脚本是否存在于脚本缓存。
    这个命令可以接受一个或者多个脚本的 SHA1 校验和信息,返回一个 1 或者 0 的列表,表示脚本存在或不存在。
  • SCRIPT LOAD
    将脚本添加到脚本缓存中,但并不立即执行该脚本。在脚本被加入到缓存之后,通过 EVALSHA 命令,可以使用脚本的 SHA1 校验和来调用这个脚本。 EVAL 命令也会将脚本添加到脚本缓存中,但是它会立即对输入的脚本进行求值。
    脚本可以在缓存中保留无限长的时间,直到执行 SCRIPT FLUSH 为止。如果给定的脚本已经在缓存里面了,那么不做动作。
    通过 SCRIPT LOAD 命令将 Lua 脚本命令缓存,生成并返回一个固定长度的 hash 字符串(即 SHA1 校验和),不管你的 Lua 脚本命令有多长,都会返回一个定长的 hash 字符串。这样做的好处是减少网络的传输,因为 Redis 客户端向服务器传输大段的 Lua 脚本命令的时候,会增加网络开销,而将 Lua 命令缓存在 Redis 中,客户端只需要传递这个固定长度的 hash 字符串即可,可以减少传输的消耗,同时脚本缓存在 Redis 中也可以防止脚本被篡改。
  • SCRIPT KILL
    杀死当前正在运行的 Lua 脚本,当且仅当这个脚本没有执行过任何写操作时,这个命令才生效。
    这个命令主要用于终止运行时间过长的脚本,比如一个因为 BUG 而发生无限循环的脚本,诸如此类。
    SCRIPT KILL 执行之后,当前正在运行的脚本会被杀死,执行这个脚本的客户端会从 EVAL 命令的阻塞当中退出,并收到一个错误作为返回值。
    另一方面,假如当前正在运行的脚本已经执行过写操作,那么即使执行 SCRIPT KILL ,也无法将它杀死,因为这是违反 Lua 脚本的原子性执行原则的。在这种情况下,唯一可行的办法是使用 SHUTDOWN NOSAVE 命令,通过停止整个 Redis 进程来停止脚本的运行,并防止不完整(half-written)的信息被写入数据库中。
  • SCRIPT DEBUG
    命令格式 SCRIPT DEBUG YES | SYNC | NO
    使用 EVAL 可以开启对脚本的调试。Redis 包含完整 Lua Debugger(即代号 LDB, Redis Lua debugger),这大大降低了复杂脚本编写的难度。 在调试模式下,Redis 既做调试服务器又做客户端,像 redis-cli 可以单步执行,设置断点,观察变量等等,更多 LDB 信息参见 Redis Lua scripts debugger。
    注意:使用开发环境 Redis 服务器调试 Lua 脚本,避免在生产环境 Redis 服务器调试。
    LDB 可以设置成两种模式:同步和异步。异步模式下,服务器会创建新的调试连接,不阻塞其它连接,同时在调试连接结束后会回滚所有的数据修改, 这可以保证再次调试时初始状态不变。同步模式下,调试过程中,服务器其它连接会被阻塞,当调试结束后,所有的数据修改会被保存。
    YES:打开非阻塞异步调试模式,调试 Lua 脚本(回退数据修改);
    SYNC:打开阻塞同步调试模式,调试 Lua 脚本(保留数据修改);
    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 脚本文件

  Redis 也可以执行服务器上的 Lua 文件。

4.1 编写 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/ 路径下。

4.2 执行 Lua 脚本文件

  执行命令: 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
文章参考:
  • 【Lua】Lua脚本及在Redis中的使用
  • EVAL简介
  • Redis使用LUA脚本
  • Redis进阶应用:Redis+Lua脚本实现复合操作

你可能感兴趣的:(shell,Linux,NoSQL,lua,redis,数据库)