Redis中使用Lua脚本

Posted by Baird on 2021-11-30

Redis中使用Lua脚本

什么是Lua

Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放。其设计目的就是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。从 Redis2.6开始, Eval命令使用内置的Lua解释器执行脚本 ,不需要单独安装 Lua

为什么使用Lua

在使用redis的过程中,我们会发现有些时候需要原子性去操作redis命令,而为了保证多条命令组合的原子性, Redis提供了简单的事务功能以及集成Lua脚本来解决这个问题。其中redis事务是基于乐观锁,lua脚本是基于redis的单线程执行命令。

好处

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

常用命令

1.EVAL

命令格式——EVAL script numkeys key [key …] arg [arg …]

- script:用到的Lua脚本,不必定义为一个Lua函数
- numkeys:指定key [key …]中key的个数,也就是指定的Lua脚本需

要处理键的数量
- key [key …]: 传递给Lua脚本零到多个键,通过KEYS[INDEX]获取
- arg [arg …]: 附加参数,通过ARGV[INDEX]获取

1
2
3
// eg1:numkeys=1,keys数组只有1个元素key1
127.0.0.1:6379> EVAL "return KEYS[1]" 1 key1
"key1"
1
2
3
// eg2:numkeys=0,arg数组元素中有1个元素value1
127.0.0.1:6379> EVAL "return ARGV[1]" 0 value1
"value1"

其中在 Lua 脚本中,可以使用两个不同函数来执行 Redis 命令,它们分别是: redis.call() redis.pcall()

2.SCRIPT LOAD - SCRIPT EXISTS
  • SCRIPT LOAD命令格式——SCRIPT LOAD script

-SCRIPT LOAD 将脚本 script 添加到Redis服务器的脚本缓存中,并不立即执行这个脚本,而是会立即对输入的脚本进行求值。并返回给定脚本的 SHA 校验和。

-下文中EVALSHA 命令中的sha1参数,就是SCRIPT LOAD 命令执行的结果。

-如果给定的脚本已经在缓存里面了,那么不执行任何操作。

1
2
3
//SCRIPT LOAD加载脚本,得到sha1值
127.0.0.1:6379> SCRIPT LOAD "redis.call('SET', KEYS[1], ARGV[1]); return 1;"
"05f21e1ea87d154c7433c59641ef1383d6ab47c9"
  • SCRIPT EXISTS命令格式——SCRIPT EXISTS sha1 [sha1 …]

-给定一个或多个脚本的SHA校验和,返回一个包含 0 和 1 的列表,表示校验和所指定的脚本是否存在在缓存中

1
2
3
4
127.0.0.1:6379> SCRIPT EXISTS 05f21e1ea87d154c7433c59641ef1383d6ab47c9
1) (integer) 1
127.0.0.1:6379> SCRIPT EXISTS 05f21e1ea87d154c7433c59641ef1383d6ab47c2
1) (integer) 0
3.EVALSHA

命令格式——EVALSHA sha1 numkeys key [key …] arg [arg …]

-在脚本被加入到缓存之后,通过EVALSHA命令,可以使用脚本的 SHA 校验和来调用这个脚本。

-脚本可以在缓存中保留无限长的时间,直到执行SCRIPT FLUSH为止。

1
2
3
4
127.0.0.1:6379> EVALSHA 05f21e1ea87d154c7433c59641ef1383d6ab47c9 1 key1 value1
(integer) 1
127.0.0.1:6379> get key1
"value1"
4.SCRIPT FLUSH

命令格式——SCRIPT FLUSH

-用于清除Redis服务端所有 Lua 脚本缓存

1
2
127.0.0.1:6379> SCRIPT FLUSH
OK
5.SCRIPT KILL

命令格式——SCRIPT FLUSH

-杀死当前正在运行的 Lua 脚本

-仅当这个脚本没有执行过任何写操作时,命令才生效

-主要用于终止运行时间过长的脚本,比如一个因为 BUG 而发生无限 loop 的脚本

注意事项

  • lua脚本中的redis操作的key最好都是通过 keys来传递,而不要写死,否则在redis cluster的情况下可能有问题
  • 我们无法清除某一个脚本的缓存,只可以清除所有的缓存,一般情况下没有必要清除,因为即使有大量的脚本也不会太占用内存
  • script kill命令只可以杀死正在运行的只读脚本,修改了数据的脚本只能使用 shutdown nosave命令杀死
  • 脚本执行的默认超时时间为 5分钟,可以通过redis.conf配置文件的lua-time-limit配置项修改
  • 脚本即使到达了超时时间,也不会停止执行,因为这违反了lua脚本的原子性

简单编写Lua脚本文件

Lua 脚本存在较多逻辑的时候,就很有必要单独编写一个独立的Lua文件,下面我们来做一个尝试:

1
2
3
4
5
6
7
8
9
10
11
12
13
redis.call('set',KEYS[1],ARGV[1])
redis.call('set',KEYS[2],ARGV[2])
local num1 = tonumber(redis.call('get',KEYS[1]))
local num2 = tonumber(redis.call('get',KEYS[2]))
if num1 > num2 then
return 0
end
if num1 == num2 then
return 1
end
if num1 < num2 then
return 2
end

将它取名并保存起来,接下来就可以在linux操作系统上执行下面的命令进行测试:

1
redis-cli --eval <文件名> key1 key2 , <value1> <value2>

SpringBoot集成Redis调用Lua脚本

其中对Redis的操作,主要用到了SpringBoot里的RedisTemplate模板,在完成所需脚本的编写后,添加到SpringBoot项目的resources目录下,目录结构为:src/resources/xxx.luaRedisTemplate提供了两种方法执行Lua脚本:

1
2
3
4
//script-脚本资源,keys-列表
<T> T execute(RedisScript<T> script,List<K> keys,Object.... args)
//加入了两个参数:为参数序列化方法以及结果序列化方法
<T> T execute(RedisScript<T> script,RedisSerializer<?> argsSerializer,RedisSerializer<T> resultSerializer,List<K> keys,Object.... args)

RedisScript参数决定了Lua脚本资源的同时,也决定了返回值类型,我们在开发中,默认使用的是DefaultRedisScript实现对象。接下来看一下实现的方式,不提供序列化方法:

1
2
3
4
5
6
7
public void execLuaWithoutSerializer(){
DefaultRedisScript<JSONObject> script = new DefaultRedisScript<>();
script.setResultType(JSONObject.class);
script.setLocation(new ClassPathResource("/xxx.lua"));
List<String> keys =ImmutableList.of("lua:id","lua:name");
JSONObject sum =redisTemplate.execute(script, keys);
}

在调用时传入了一个List,存储两个Key值,需要注意调用时设置的ResultType必须与脚本中的返回类一致。

关于更具体的使用,可能会在下次博客中,跟处理lua限流操作中遇到的一些问题放在一起讲。