Redis中的Lua脚本入门

Redis中的Lua脚本入门

简介

​ Redis是高性能的键值对内存数据库,除了做缓存中间件的基本作用外还有很多用途,比如布隆过滤器、分布式锁等。Redis的单个命令都是原子性的,有时候我们希望能够组合多个Redis命令,并希望能保证复合操作的原子性。Redis在2.6版本中引入了一个特性来解决这个问题,这就是Redis执行Lua脚本。

Lua脚本基础语法

​ Lua语言广泛作为其它语言的嵌入脚本,其语法简单,小巧,源码一共才200多K。接下来介绍Lua脚本的一些常用语法。

注释

1
2
3
4
5
--单行注释
--[[
多行注释
多行注释
]]

变量

1
2
3
4
5
6
7
8
9
10
11
12
--常见的两种变量:全局变量、局部变量
--变量默认为全局变量,使用local修饰的变量为局部变量
--即使在函数里不带local声名的变量也是全局变量
local a = 1

--标识符规则:字母、数字、下划线开头,后可接数字
--最好不要使用下划线加大写字母的标识符,因为Lua的保留字也是这样的

--变量交换
a, b = 0, 1
a, b = b, a
print(a) -->1

数据类型

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
--string
print(type("Hello world"))
--进行算数运算时,会进行隐式转换,相当于tonumber()
--字符串连接使用..
--使用#计算字符串长度,如#"aaa" --> 3

--number
print(type(10.4*3))

--function
print(type(print))

--boolean
print(type(true))
--nil与false为假,其余都为真

--nil
print(type(nil))

--table, 通过构造表达式({})创建
a = {}--创建一个空table
a["key"] = "value"
key = 10
a[key] = 22
for k, v in pairs(a) do
print(k .. " : " .. v)
end
--[[输出结果
key : value
10 : 33
]]
--table默认初始索引以1开始
b = {"a", "b"}
for k, v in pairs(b) do
print("k : ", k)
end
--[[输出结果
K : 1
K : 2
]]

--创建table的时候给索引
c = {k1="v1", k2=2}
for k, v in pairs(c) do
print(k .. " : " .. v)
end
--[[输出结果
k1 : v1
k2 : 2
]]

--table解构,unpack或table.unpack,参数:table,起始索引,结束索引
a = {1, 2, 3, 4, 5}
print(table.unpack(a, 1, #a)) -->1 2 3 4 5

判断结构

1
2
3
4
5
6
7
8
9
--变量
local a = 10
if a < 10 then
print('a小于10')
elseif a < 20 then
print('a小于20,大于等于10')
else
print('a大于等于20')
end

循环结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
--while循环
while(condition)
do
statements
end

--如下
a = 10
while(a < 20)
do
a = a + 1
end
print("a 的值为:", a) -->a 的值为: 20

--for循环
--range循环,起点,终点,步长(默认1)
for i=10,1,-1 do
print(i)
end
--foreach循环
b = {"a", "b"}
for k, v in pairs(b) do
print("k : ", k)
end

函数

1
2
3
4
5
6
7
8
--函数声明
--函数可以返回多个返回值,如return a, b
function 函数名(参数列表)

函数逻辑代码

return 返回值
end

运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
--[[
算数运算符:
+ 加法
- 减法,负号
* 乘法
/ 除法
% 取余
^ 乘幂
// 整除运算符

关系运算符:
== 等于
~= 不等于
> 大于
< 小于
>= 大于等于
<= 小于等于

逻辑运算符
and 逻辑与
or 逻辑或
not 逻辑非
]]

Redis中的Lua脚本

EVAL命令

​ 在Redis中使用EVAL命令执行Lua脚本。在Lua脚本中使用redis.call()执行Redis命令。Lua脚本在Redis中是以原子方式执行的,在Redis服务器执行EVAL命令时,在命令执行完毕并向调用者返回结果之前,只会执行当前命令指定的Lua脚本包含的所有逻辑,其它客户端发送的命令将被阻塞,直到EVAL命令执行完毕为止。

image-20241205160857370

  • EVAL 命令
  • script Lua 脚本
  • numkeys 指定Lua脚本键的数量,即 KEYS数组的长度
  • key 传递给Lua脚本零到多个键,空格隔开,在Lua 脚本中通过 **KEYS[index]**来获取对应的值,其中1 <= INDEX <= numkeys
  • arg是传递给脚本的零到多个附加参数,空格隔开,在Lua脚本中通过**ARGV[index]**来获取对应的值

redis.call()

​ 在Lua脚本中使用redis.call()执行Redis命令。参数个数可变。

1
2
3
eval "return redis.call('set', KEYS[1], ARGV[1])" 1 jujuyi hello

该命令相当于执行了set jujuyi hello

Redis限流Lua脚本案例

​ 通过Lua脚本,可以实现很多复杂功能,下面给出一个通过Redis的Lua脚本进行限流的案例(时间窗口限流)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--用户名
local username = KEYS[1]
--时间窗口,单位:秒
local timeWindow = tonumber(ARGV[1])

local rediskey = "user_access_limit:" .. username

local currentAccessCount = redis.call("INCR", rediskey)
--时间窗口内第一次访问
if currentAccessCount == 1 then
redis.call("EXPIRE", rediskey, timeWindow)
end

-- 返回当前时间窗口访问次数,通过返回值进行放行或拦截
return currentAccessCount
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
//Spring环境
private String timeWindow = "2";
private int limit = 5;

private String LUA_SCRIPT_PATH = "mylua.lua";

private StringRedisTemplate stringRedisTemplate;

public void accessLimit(String username){
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(LUA_SCRIPT_PATH)));
redisScript.setResultType(Long.class);
Long result;
try {
result = stringRedisTemplate.execute(
redisScript,
List.of(username),
timeWindow);
} catch (Throwable ex) {
log.error("执行LUA脚本出错", ex);
throw ex;
}
if (result == null || result > limit) {
throw new RuntimeException("您的操作过于频繁");
}
}

扩展

​ Redis还支持Lua脚本管理,可通过script load保存Lua脚本,会返回一个脚本标识,后续可通过evalsha命令执行已经保存的脚本以减少脚本的反复传输。