Redis使用LUA脚本

Posted cj_eryue

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis使用LUA脚本相关的知识,希望对你有一定的参考价值。

一、简介

1、什么是Lua?
Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放。
其设计目的就是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。因为广泛的应用于:游戏开发、独立应用脚本、Web 应用脚本、扩展和数据库插件等。
比如:Lua脚本用在很多游戏上,主要是Lua脚本可以嵌入到其他程序中运行,游戏升级的时候,可以直接升级脚本,而不用重新安装游戏。

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

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

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

二、Redis中Lua的常用命令

命令不多,就下面这几个:
- EVAL
- EVALSHA
- SCRIPT LOAD - SCRIPT EXISTS
- SCRIPT FLUSH
- SCRIPT KILL

2.1 EVAL命令 

命令格式:eval script numkeys key [key …] arg [arg …]

  • script参数是一段 Lua5.1 脚本程序。
  • numkeys指定后续参数有几个key,即:key [key …]中key的个数。如没有key,则为0 。
  • key [key …] 从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key)。在Lua脚本中通过KEYS[1], KEYS[2]获取。
  • arg [arg …] 附加参数。在Lua脚本中通过ARGV[1],ARGV[2]获取。

看看例子:

①定义一个KEYS[]数组,numkeys=1,值为 keyTest

127.0.0.1:6379> eval "return KEYS[1]" 1 keyTest
"keyTest"

②定义一个KEYS[]数组,numkeys=2,值为 keyTest1 keyTest2,返回KEYS[2]

127.0.0.1:6379> eval "return KEYS[2]" 2 keyTest1 keyTest2
"keyTest2"

③定义一个KEYS[]数组,numkeys=2,值为 keyTest1 keyTest2,返回KEYS[1],KEYS[2]

127.0.0.1:6379> eval "return KEYS[1],KEYS[2]" 2 keyTest1 keyTest2
1) "keyTest1"
2) "keyTest2"

④定义一个ARGV[]数组,KEYS[]无元素,值为 argvTest 

127.0.0.1:6379> eval "return ARGV[1]" 0 argvTest
"argvTest"

⑤定义一个ARGV[]数组,KEYS[]无元素,值为 argvTest1,argvTest2

​​​​​127.0.0.1:6379> eval "return ARGV[1],ARGV[2]" 0 argvTest1 argvTest2
1) "argvTest1"
2) "argvTest2"

⑥使用redis.call()去调用redis的命令

127.0.0.1:6379> eval "return redis.call('set',KEYS[1],ARGV[1]),redis.call('get',KEYS[2])" 2 name name cjian
1) OK
2) "cjian"

⑥设置key为name,value为cjian的String数据,并设置20s过期时间

127.0.0.1:6379> eval "return redis.call('set',KEYS[1],ARGV[1]),redis.call('expire',KEYS[2],ARGV[2])" 2 name name cjian 20
1) OK
2) (integer) 1
127.0.0.1:6379> ttl name
(integer) 16

在 Lua 脚本中,可以使用两个不同函数来执行 Redis 命令,它们分别是: redis.call() 和 redis.pcall()
这两个函数的唯一区别在于它们使用不同的方式处理执行命令所产生的错误,差别如下:

错误处理
当 redis.call() 在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,错误的输出信息会说明错误造成的原因。

redis.pcall() 出错时并不引发(raise)错误,而是返回一个带 err 域的 Lua 表(table),用于表示错误:

127.0.0.1:6379> lpush list a
(integer) 1
127.0.0.1:6379> eval "return redis.call('get','list')"
(error) ERR wrong number of arguments for 'eval' command
127.0.0.1:6379> eval "return redis.call('get','list')" 0
(error) ERR Error running script (call to f_ba3d4ebf4203769966ef6de3b9450e9ff4436115): @user_script:1: WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> eval "return redis.pcall('get','list')" 0
(error) WRONGTYPE Operation against a key holding the wrong kind of value

2.2 SCRIPT LOAD命令 和 EVALSHA命令 

SCRIPT LOAD命令格式:SCRIPT LOAD script
EVALSHA命令格式:EVALSHA sha1 numkeys key [key …] arg [arg …]

这两个命令放在一起讲的原因是:EVALSHA 命令中的sha1参数,就是SCRIPT LOAD 命令执行的结果。

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

在脚本被加入到缓存之后,在任何客户端通过EVALSHA命令,可以使用脚本的 SHA1 校验和来调用这个脚本。脚本可以在缓存中保留无限长的时间,直到执行SCRIPT FLUSH为止。

127.0.0.1:6379> script load "redis.call('set',KEYS[1],ARGV[1]);redis.call('expire',KEYS[1],ARGV[2]);return 1;"
"b53db7fdd933555fba818bbdb07d30d0701b3fad"
127.0.0.1:6379> evalsha b53db7fdd933555fba818bbdb07d30d0701b3fad 1 age 10 60
(integer) 1
127.0.0.1:6379> get age
"10"
127.0.0.1:6379> ttl age
(integer) 50
127.0.0.1:6379> ttl age
(integer) 47

2.3 SCRIPT EXISTS 命令

命令格式:SCRIPT EXISTS sha1 [sha1 …]
作用:给定一个或多个脚本的 SHA1 校验和,返回一个包含 0 和 1 的列表,表示校验和所指定的脚本是否已经被保存在缓存当中

127.0.0.1:6379> script exists b53db7fdd933555fba818bbdb07d30d0701b3fad
1) (integer) 1
127.0.0.1:6379> script exists b53db7fdd933555fba818bbdb07d30d0701b3fad11
1) (integer) 0
127.0.0.1:6379> script exists b53db7fdd933555fba818bbdb07d30d0701b3fad b53db7fdd933555fba818bbdb07d30d0701b3fad11
1) (integer) 1
2) (integer) 0

2.4 SCRIPT FLUSH 命令

命令格式:SCRIPT FLUSH
作用:清除Redis服务端所有 Lua 脚本缓存

127.0.0.1:6379> script exists b53db7fdd933555fba818bbdb07d30d0701b3fad
1) (integer) 1
127.0.0.1:6379> script flush
OK
127.0.0.1:6379> script exists b53db7fdd933555fba818bbdb07d30d0701b3fad
1) (integer) 0

2.5 SCRIPT KILL 命令

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

假如当前正在运行的脚本已经执行过写操作,那么即使执行SCRIPT KILL,也无法将它杀死,因为这是违反 Lua 脚本的原子性执行原则的。在这种情况下,唯一可行的办法是使用SHUTDOWN NOSAVE命令,通过停止整个 Redis 进程来停止脚本的运行,并防止不完整(half-written)的信息被写入数据库中。

三、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

功能为:如果KEYS[1]的value 与 ARGV[1] 相同,则将KEYS[1]的value设为ARGV[2],并返回1,否则返回0,一个cas操作。

将文件命令为compareAndSet.lua,放到redis的bin目录下。

准备数据

127.0.0.1:6379> set name cjian
OK
127.0.0.1:6379> get name
"cjian"

3.2 执行Lua脚本文件

执行命令: redis-cli -a 密码 --eval Lua脚本路径 key [key …] ,  arg [arg …] 
如:redis-cli.exe --eval compareAndSet.lua name, cjian cjian2 

脚本路径后紧跟key [key …],相比命令行模式,少了numkeys这个key数量值
key [key …] 和 arg [arg …] 之间的“ , ”,英文逗号前后必须有空格,否则死活都报错 

E:\\software\\Redis-x64-5.0.14.1>redis-cli.exe --eval compareAndSet.lua name , cjian cjian2
(integer) 1

E:\\software\\Redis-x64-5.0.14.1>redis-cli.exe --eval compareAndSet.lua name , cjian cjian2
(integer) 0

四、实例:使用Lua控制IP访问频率

需求:实现一个访问频率控制,某个IP在短时间内频繁访问页面,需要记录并检测出来,就可以通过Lua脚本高效的实现。
PS:本实例针对固定窗口的访问频率,而动态的非滑动窗口。即:如果规定一分钟内访问10次,记为超限。在本实例中前一分钟的最后一秒访问9次,下一分钟的第1秒又访问9次,不计为超限。
脚本如下:

local visitNum = redis.call('incr', KEYS[1])
-- 第一次访问后,设置有效时间
if visitNum == 1 then
        redis.call('expire', KEYS[1], ARGV[1])
end
-- 如果超过限制的访问次数,返回0
if visitNum > tonumber(ARGV[2]) then
        return 0
end

return 1;

 被限制的ip为127.0.0.1,10秒内只可以访问2次:

E:\\software\\Redis-x64-5.0.14.1>redis-cli.exe --eval visitNum.lua 127.0.0.1 , 10 2
(integer) 1
E:\\software\\Redis-x64-5.0.14.1>redis-cli.exe --eval visitNum.lua 127.0.0.1 , 10 2
(integer) 1
E:\\software\\Redis-x64-5.0.14.1>redis-cli.exe --eval visitNum.lua 127.0.0.1 , 10 2
(integer) 0
E:\\software\\Redis-x64-5.0.14.1>redis-cli.exe --eval visitNum.lua 127.0.0.1 , 10 2
(integer) 0
E:\\software\\Redis-x64-5.0.14.1>redis-cli.exe --eval visitNum.lua 127.0.0.1 , 10 2
(integer) 0
E:\\software\\Redis-x64-5.0.14.1>redis-cli.exe --eval visitNum.lua 127.0.0.1 , 10 2
(integer) 0
E:\\software\\Redis-x64-5.0.14.1>redis-cli.exe --eval visitNum.lua 127.0.0.1 , 10 2
(integer) 0
-- 10 秒后再次执行
E:\\software\\Redis-x64-5.0.14.1>redis-cli.exe --eval visitNum.lua 127.0.0.1 , 10 2
(integer) 1
E:\\software\\Redis-x64-5.0.14.1>redis-cli.exe --eval visitNum.lua 127.0.0.1 , 10 2
(integer) 1
E:\\software\\Redis-x64-5.0.14.1>redis-cli.exe --eval visitNum.lua 127.0.0.1 , 10 2
(integer) 0

本文参考:Redis中使用Lua脚本(一) - 知乎

以上是关于Redis使用LUA脚本的主要内容,如果未能解决你的问题,请参考以下文章

redis lua脚本有啥用

Redis与Lua脚本

redis在使用lua脚本以及实现redis分布式锁

redis使用lua脚本

redis使用lua脚本

Redis Lua脚本的详细介绍以及使用入门