Lua脚本-redis

1 在redis中调用lua脚本

使用eval方法,语法格式:

1
2
3
4
5
6
7
eval lua-script key-num [key1 key2 key3 ...] [value1 value2 value3 ...]

//eval 代表执行Lua语言的命令
//lua-script 代表Lua语言脚本内容
//key-num 表示参数中有多少个key,需要注意的是Redis中的key是从1开始的,如果没有key的参数,那么写0
//[key1 key2 key3 ...]是key作为参数传递给Lua语言,也可以不写,但是需要和key-num的个数对应起来
//[value1 value2 value3 ...]参数的value值,一一对应,可填可不填

示例:返回一个字符串,0个参数:

1
eval "return 'Hello World'" 0

实际上,Lua脚本在Redis里面真正的用途是用来执行redis命令。

2 在Lua脚本中调用redis命令

2.1 命令格式

使用redis.call(command, key, [param1, param2 ……])进行操作。语法格式:

1
2
3
4
redis.call(command, key [param1, param2, ...])
-- command是命令,包括set、get、del等
-- key是被操作的键
-- param1, param2 ...代表给key的参数

一个简单的案例,让Lua脚本执行set qingshan 2673

1
2
eval "return redis.call('set','qingshan','2673')"  //写死值
eval "return redis.call('set', KEYS[1], ARGV[1])" 1 qingshan 2673 //参数传递

在redis-cli中直接写Lua脚本不够方便,也不能实现编辑和复用,通常我们会把lua脚本凡在文件中,然后执行这个文件。

2.2 Lua脚本文件

创建脚本文件

1
2
cd /usr/local/soft/redis-6.0.9/src
vim test.lua

Lua脚本内容,先赋值,再取值:

1
2
redis.call('set','qingshan','lua666')
return redis.call('get','qingshan')

调用脚本文件

1
2
cd /usr/local/soft/redis-6.0.9/src
redis-cli --eval test.lua 0

2.3 案例:对IP进行限流

需求:每个用户再X秒内只能访问Y次。设计思路:

首先是数据类型。用String的key记录IP,用value记录访问的次数。几秒钟和几次哟啊用参数动态传进去。拿到IP以后,对IP+1。如果是第一次访问,对key设置国企时间(参数1).否则判断次数,超过限定次数(参数2),返回0。如果没有超过次数返回1。超过时间,key国企之后,可以再次访问。

KEY[1]是IP,ARGV[1]是过期时间X,ARGV[2]是限制访问的次数Y。

1
2
3
4
5
6
7
8
9
10
11
-- ip_limit.lua
-- IP限流,对某个IP频率进行校址,6秒钟访问10次
local num=redis.call('incr',KEYS[1])
if tonumber(num)==1 then
redis.call('expire',KEYS[1],ARGV[1])
return 1
elseif tonumber(num)>tonumber(ARGV[2]) then
return 0
else
return 1
end

6秒钟内限制访问10次,调用测试(连续调用10次)

1
redis-cli --eval ip_limit.lua app:ip:limit:192.168.8.111 , 6 10

Java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class LuaTest {
public static void main(String[] args) {
Jedis jedis = getJedisUtil();
jedis.eval("return redis.call('set',KEYS[1],ARGV[1])", 1,"test:lua:key","qingshan2673lua");
System.out.println(jedis.get("test:lua:key"));
for(int i=0; i<10; i++){
limit();
}
}

/**
* 10秒内限制访问5次
*/
public static void limit(){
Jedis jedis = getJedisUtil();
// 只在第一次对key设置过期时间
String lua = "local num = redis.call('incr', KEYS[1]) \n" +
"if tonumber(num) == 1 then\n" +
"\t redis.call('expire', KEYS[1], ARGV[1]) \n" +
"\t return 1 \n" +
"elseif tonumber(num) > tonumber(ARGV[2]) then \n" +
"\t return 0 \n" +
"else \n" +
"\t return 1 \n" +
"end \n";
Object result = jedis.evalsha(jedis.scriptLoad(lua), Arrays.asList("localhost"), Arrays.asList("10", "5"));
System.out.println(result);
}

private static Jedis getJedisUtil() {
String ip = ResourceUtil.getKey("redis.host");
int port = Integer.valueOf(ResourceUtil.getKey("redis.port"));
String password = ResourceUtil.getKey("redis.password");
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
JedisPool pool = new JedisPool(jedisPoolConfig, ip, port, 10000, password);
return pool.getResource();
}

}

2.4 缓存Lua脚本

为什么要缓存

​ 在Lua脚本比较长的情况下,如果多次调用脚本都需要把整个脚本传给Redis服务端,会产生比较大的网络开销。为了解决这个问题,Redis可以缓存Lua脚本并生成SHA1摘要码,后面可以直接通过摘要码来执行Lua脚本。

如何缓存

这里面涉及到两个命令,首先是在服务端缓存lua脚本生成一个摘要码,用script load命令

1
script load "return Hello World"

第二个命令是通过摘要码执行缓存的脚本:

1
evalsha "摘要码" 0

自乘案例

Redis有incrby这样的自增命令,但是没有自乘。比如乘以3,乘以5

1
set num 2

写一个自乘的运算,让它乘以后面的参数:

1
2
3
4
5
6
7
8
9
local curVal=redis.call("get",KEYS[1])
if curVal==false then
curVal=0
else
curVal=tonumber(curVal)
end
curVal=curVal*tonumber(ARGV[1])
redis.call("set",KEYS[1],curVal)
return curVal

这个命令变成串行,语句之间使用分号隔开,Script load命令(redis客户端执行)

1
script locad 'local curVal=redis.call("get",KEYS[1]);if curVal==false then curVal=0 else curVal=tonumber(curVal) end;curVal=curVal*tonumber(ARGV[1]);redis.call("set",KEYS[1],curVal);return curVal'

调用

1
evalsha "摘要码" 1 num 6

2.5 脚本超时

Redis的指令执行本身是单线程的,这个线程还要执行客户端的Lua脚本,如果Lua脚本执行超时或者陷入了死循环,是不是没有办法为客户端提供服务了?它会导致其他的命令都会进入等待状态。为了防止这种情况,首先,脚本执行有一个超时时间,默认为5秒钟。

1
lua-time-limit 5000

超过5秒钟,其他客户端的命令不会等待,而是直接返回BUSY错误。这样也不行,不能一直拒绝其他客户端的命令执行。在提示中看到有两个命令可以使用,第一个是script kill,终止脚本的执行。但是需要注意:并不是所有的lua脚本执行都可以kill。如果当前执行的lua脚本对Redis的数据进行了修改(set,DEL、等),那么通过script kill命令是不能终止脚本运行的。为什么包含修改的脚本不能中断?因为要保证脚本运行的原子性。如果脚本执行了一部分被终止,那就违背了脚本原子性的目标。遇到这种情况,只能通过shutdown nosave命令,直接把redis服务停掉。正常关机是shutdown。Shutdown nosave和shutdown的区别在于shutdown nosave不会进行持久化操作,意味着发生在上一个快照后的数据库修改都会丢失。


Lua脚本-redis
http://www.zivjie.cn/2023/02/25/lua/Lua-redis/
作者
Francis
发布于
2023年2月25日
许可协议