redis:redis与lua

redis中为什么引入Lua脚本?

遇到的问题:

  • redis是高性能的key-value内存数据库,在部分场景下,是对关系数据库的良好补充
  • redis提供了非常丰富的指令集,官网上提供了200多个命令。但是在某些特定领域,需要扩充若干指令的原子性执行时,仅使用原生命令无法完成
  • redis为这样的用户常见提供了lua脚本支持,用于可以向服务器发送lua脚本来执行自定义动作,获取脚本的响应数据。redis服务器会单线程原子性执行lua脚本,保证lua脚本在处理的过程中不会被任意其他请求打断

redis在2.6版本推出了 lua 脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行。使用脚本的好处如下:

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

redis执行Lua

在Redis中使用Lua 在Redis中执行Lua脚本有两种方法: eval和evalsha。

eval

用 Lua 解释器执行脚本。

语法

redis 127.0.0.1:6379> EVAL script numkeys key [key ...] arg [arg ...] 

参数说明

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

原理

如果lua脚本较长,还可以使用redis-cli --eval直接执行文件。eval命令和–eval参数本质是一样的

  • 客户端编写好lua脚本
  • 客户端把脚本作为字符串发送给服务端
  • 服务端会将执行结果返回给客户端。

整个过程如下图
在这里插入图片描述

实例

127.0.0.1:6379> eval 'return "hello ".. KEYS[1]..ARGV[1]' 1 redis word
"hello redisword"
  • 此时KEYS[1]=“redis”,ARGV[1]=“world”
  • 所以最终的返回结果 是"hello redisworld"。
redis 127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

evalsha

  • evalsha根据给定的sha1校验码,执行缓存在服务器中的脚本,将脚本

  • 将脚本缓存到服务器的操作可以通过 SCRIPT LOAD 命令进行。

语法

redis 127.0.0.1:6379> EVALSHA sha1 numkeys key [key ...] arg [arg ...] 

参数说明:

  • sha1 : 通过 SCRIPT LOAD 生成的 sha1 校验码。
  • numkeys: 用于指定键名参数的个数。
  • key [key …]: 从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 1 为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。
  • arg [arg …]: 附加参数,在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。

原理

  • 首先要将lua脚本加载到redis服务端,得到该脚本的SHA1校验和
  • evalsha命令使用SHA1作为参数可以直接执行对应的lua脚本,避免每次发送lua脚本的开销
  • 这样客户端就不需要每次执行脚本内容,而脚本也会常驻在服务端,脚本功能得到了复用

在这里插入图片描述

实例

实例

加载脚本

  • script load命令可以将脚本内容加载到Redis内存中
  • 例如下 面将lua_get.lua加载到Redis中,得到SHA1 为:“7413dc2440db1fea7c0a0bde841fa68eefaf149c”
# redis-cli script load "$(cat lua_get.lua)"
"7413dc2440db1fea7c0a0bde841fa68eefaf149c"
  • 执行脚本
127.0.0.1:6379> evalsha 7413dc2440db1fea7c0a0bde841fa68eefaf149c 1 redis world 
"hello redisworld"

实例

# 加载脚本
redis 127.0.0.1:6379> SCRIPT LOAD "return 'hello moto'"
"232fd51614574cf0867b83d384a5e898cfd24e5a"

# 执行脚本

redis 127.0.0.1:6379> EVALSHA "232fd51614574cf0867b83d384a5e898cfd24e5a" 0
"hello moto"

Lua的RedisAPI

Lua可以使用redis.call函数实现对redis的访问,例如下面代码是Lua使用redis.call调用了Redis的set和get操作:


redis.call("set", "hello", "world")
redis.call("get", "hello")

放在Redis的执行效果如下:

127.0.0.1:6379> eval 'redis.call("set", "hello", "world")' 0
(nil)
127.0.0.1:6379> eval 'return redis.call("get", KEYS[1])' 1 hello
"world"
127.0.0.1:6379> 

除此之外,lu还可以使用redis.pcall函数实现对redis的调用。redis.call和redis.pcall的不同在于,如果redis.call执行失败,那么脚本执行结束会直接返回错误,而redis.pcall会忽略错误继续执行脚本,所以在实际开发中需要根据具体的应用场景进行函数的选择。

redis如何管理lua脚本

script load

作用

  • Redis Script Load 命令用于将脚本 script 添加到脚本缓存中,但并不立即执行这个脚本
  • EVAL 命令也会将脚本添加到脚本缓存中,但是它会立即对输入的脚本进行求值。
  • 如果给定的脚本已经在缓存里面了,那么不执行任何操作。
  • 在脚本被加入到缓存之后,通过 EVALSHA 命令,可以使用脚本的 SHA1 校验和来调用这个脚本。
  • 脚本可以在缓存中保留无限长的时间,直到执行 SCRIPT FLUSH 为止。

语法

redis 127.0.0.1:6379> SCRIPT LOAD script

返回值

给定脚本的 SHA1 校验和

实例

redis 127.0.0.1:6379> SCRIPT LOAD "return 1"
"e0e1f9fabfc9d4800c877a703b823ac0578ff8db"

script exists

作用

  • Redis Script Exists 命令用于校验指定的[多个]脚本是否已经被保存在缓存当中。

语法

SCRIPT EXISTS sha1 [sha1 ...]

返回值

  • 一个列表,包含 0 和 1 ,前者表示脚本不存在于缓存,后者表示脚本已经在缓存里面了。
  • 列表中的元素和给定的 SHA1 校验和保持对应关系,比如列表的第三个元素的值就表示第三个 SHA1 校验和所指定的脚本在缓存中的状态。

script flush

作用

  • Redis Script Flush 命令用于清除所有 Lua 脚本缓存。

语法

redis 127.0.0.1:6379> SCRIPT FLUSH

返回值

总是返回 OK

实例

redis 127.0.0.1:6379> SCRIPT FLUSH
OK

script kill

作用

  • 用于杀掉正在执行的Lua脚本,当且仅当这个脚本没有执行过任何写操作时,这个命令才生效。
  • 如果lua脚本比较耗时,甚至lua脚本存在问题,那么此时lua脚本的执行会阻塞redis,直到脚本执行完毕或者外部进行干预将其结束

语法

redis 127.0.0.1:6379> SCRIPT KILL

实例

下面模拟一个Lua脚本阻塞的情况进行说明。

执行Lua脚本,当前客户端会阻塞:

127.0.0.1:6379> eval 'while 1==1 do end' 0
..卡在这里不动了,因为这个脚本一直做死循环

在另外一个客户端执行一个命令:

127.0.0.1:6379> get world
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.

此时Redis已经阻塞,无法处理正常的调用,可以使用script kill结束当前正在执行的Lua脚本:

127.0.0.1:6379> script kill
OK

可在阻塞的那一端看到(脚本被杀死的信息):

127.0.0.1:6379> eval 'while 1==1 do end' 0
(error) ERR Error running script (call to f_c045d3ae3b3eca855e00c772db40aa560b3a1fc8): @user_script:1: Script killed by user with SCRIPT KILL... 
(38.91s)

注意:如果当前Lua脚本已经执行过写操作,那么script kill将不会生效。例如:

127.0.0.1:6379> eval 'while 1==1 do redis.call("set","k","v") end' 0   --一直执行写操作

在另一个客户端使用script kill杀不掉:

127.0.0.1:6379> script kill
(error) UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.

此时唯一的办法就是重启服务,必须使用shutdown nosave

--不能以这种方法关
[root@Redis ~]# redis-cli shutdown
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
--必须这样
127.0.0.1:6379> shutdown nosave
not connected>

补充:redis提供了一个lua-time-limit参数,默认时间是5s,它是Lua脚本的超时时间,但是这个超时时间仅仅是当Lua脚本超过lua-time-limit后,向其他命令发送BUST的信号,但是不会停止掉服务和客户端的执行脚本

案例

案例1

下面以一个例子说明Lua脚本的使用,当前列表记录着热门用户的id,假设这个列表有五个元素,如下图所示:

127.0.0.1:6379> lrange hot:user:list 0 -1
1) "user:1:ratio"
2) "user:8:ratio"
3) "user:3:ratio"
4) "user:4:ratio"
5) "user:72:ratio"

user:{id}:ratio代表用户的热度,它本身又是一个字符串类型的键:

127.0.0.1:6379> mget user:1:ratio user:8:ratio user:3:ratio user:4:ratio user:72:ratio
1) "987"
2) "763"
3) "556"
4) "400"
5) "101"

现要求将列表内所有的键对应热度做加1操作,并且保证是原子性执行,此功能可以利用Lua脚本来实现。

脚本内容如下:

--将列表中所有元素取出,赋值给mylist
local mylist = redis.call("lrange",KEYS[1],0, -1)
--定义局部变量count=0,这个count就是最后incr的总次数
local count = 0
--遍历mylist中所有元素,每次做完count自增,最后返回count
for index,key in ipairs(mylist)
do
    redis.call("incr",key)
    count = count + 1
end
return count

执行脚本:

[root@Redis script.lua]# redis-cli --eval mylua.lua hot:user:list
(integer) 5

执行后用户热度自增1:

127.0.0.1:6379> mget user:1:ratio user:8:ratio user:3:ratio user:4:ratio user:72:ratio
1) "987"
2) "763"
3) "557"
4) "401"
5) "101"
  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值