Redis系列之Lua脚本整合
概述
Redis从2.6版支持Lua。Lua脚本可以编译、运行在任何平台上;一个脚本即是一个原子事务。
Lua
官网,一门小巧精悍的脚本语言。设计用于嵌入到应用程序中,为应用程序提供灵活的扩展、定制功能,与C/C++代码可相互调用。还可用作配置文件。Lua-JIT项目,旨在提供在特定平台上的即时编译功能。
特性:
- 变量名没有类型,值才有类型,变量名在运行时可与任何类型的值绑定;
- 语言只提供一种数据结构:表(table),混合数组+哈希,可以用任何类型的值作为 key 和 value。提供一致且富有表达力的表构造语法,使得Lua很适合描述复杂的数据;
- 函数是一等类型,支持匿名函数和正则尾递归(proper tail recursion);
- 支持词法定界(lexical scoping)和闭包(closure);
- 提供 thread 类型和结构化的协程(coroutine)机制,在此基础上可方便实现协作式多任务;
- 运行期能编译字符串形式的程序文本并载入虚拟机执行;
- 通过元表(metatable)和元方法(metamethod)提供动态元机制(dynamic meta-mechanism),从而允许程序运行时根据需要改变或扩充语法设施的内定语义;
- 能方便地利用表和动态元机制实现基于原型(prototype-based)的面向对象模型;
- 从 5.1 版开始提供完善的模块机制,从而更好地支持开发大型的应用程
集成
Redis支持大部分Lua标准库:
库名 | 说明 |
---|---|
Base | 提供一些基础函数 |
String | 提供用于字符串操作的函数 |
Table | 提供用于表操作的函数 |
Math | 提供数学计算函数 |
Debug | 提供用于调试的函数 |
另外,在脚本中可使用redis.call
函数调用redis命令:
redis.call('set', 'foo', 'bar')
local value=redis.call('get', 'foo') --value的值为bar
Redis命令的返回值有5种类型,redis.call
函数会将这5种类型的返回值转换成对应的Lua的数据类型:
redis返回值类型 | Lua数据类型 |
---|---|
整数 | 数字类型 |
字符串 | 字符串类型 |
多行字符串 | table类型,数组形式 |
状态 | table类型(只有一个ok字段存储状态信息) |
错误 | table类型(只有一个err字段存储错误信息) |
空结果 | false |
redis还提供redis.pcall
函数,功能与redis.call
相同,唯一的区别是当命令执行出错时,redis.pcall
会记录错误并继续执行,而redis.call
会直接返回错误,不会继续执行。在脚本中可以使用return语句将值返回给客户端,如果没有执行return语句则默认返回nil。
配置
在redis.conf
配置文件中:lua-time-limit 5000
。为了防止某个脚本执行时间过长,导致Redis无法提供服务,Redis提供lua-time-limit参数限制脚本的最长运行时间,默认为5秒钟。当脚本运行时间超过这一限制后,Redis将开始接受其他命令但不会执行(以确保脚本的原子性,因为此时脚本并没有被终止),而是会返回“BUSY”错误。
优势
- 减少网络开销:可以将多个请求通过脚本的形式一次发送,减少网络时延和请求次数
- 原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。无需担心会出现竞态条件,无需使用事务
- 代码复用:客户端发送的脚本会永久存在Redis中,其他客户端可复用
- 速度快:JIT编译器可以显著地提高性能
- 可移植:Lua基于C,只要是有ANSI C 编译器的平台都可以编译,甚至浏览器也可以完美使用(翻译成JS)
- 源码小巧:2w行C代码,可以编译进182K的可执行文件,加载快,运行快
命令
Redis支持Lua脚本功能,随之新增的几个命令(script debug除外,是Redis 3.2版本引入的命令)。
eval
参考:eval文档
命令参数:EVAL script numkeys key [key ...] arg [arg ...]
命令解读:
- script参数是一段Lua脚本程序,会被运行在Redis服务器上下文中,这段脚本不必(也不应该)定义为一个Lua函数
- numkeys参数指定键名参数的个数;当脚本不需要任何参数时,也不能省略这个参数(设为0)
- 键名参数
key [key ...]
,表示在脚本中所用到的那些Redis键(key),可在Lua中通过全局变量KEYS数组,用1为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推) - 附加参数
arg [arg ...]
,可在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)
演示:
1608(10.114.31.113:6408)> eval "return {KEYS[1],ARGV[1]}" 1 testKey testValue
1) "testKey"
2) "testValue"
1608(10.114.31.113:6408)> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 username age jack 20
"CROSSSLOT Keys in request don't hash to the same slot"
参考:redis-cross-slot-error,大意是在一个分布式Redis集群里,key会被划分到不同的槽中,不同节点会拥有散列槽的一个子集。
In a cluster topology, the keyspace is divided into hash slots. Different nodes will hold a subset of hash slots.Multiple keys operations, transactions, or Lua scripts involving multiple keys are allowed only if all the keys involved are in hash slots belonging to the same node.
Redis集群实现了所有非分布式版本的单key命令。多个key的操作、事务或者lua脚本调用多个key是允许的,前提是:所有被调用的key都在一个节点的hash槽中就可以。
解决方法
使用Hash Tags强制所有的key属于一个节点。
文件
lua脚本较长时,可放置在文件中,
$ redis-cli --eval path/to/redis.lua KEYS[1] KEYS[2] ... , ARGV[1] ARGV[2] ...
–eval,告诉redis-cli读取并运行后面的lua脚本
KEYS和ARGV中间的 ‘,’ 两边的空格,不能省略。
EVALSHA
参考:evalsha文档
在脚本比较长的情况下,若每次调用脚本都需要将整个脚本传给Redis会占用较多的带宽。为解决这个问题,Redis提供EVALSHA命令,允许开发者通过脚本内容的SHA1摘要来执行脚本,该命令的用法和EVAL一样,只不过是将脚本内容替换成脚本内容的SHA1摘要。
Redis在执行EVAL命令时会计算脚本的SHA1摘要并记录在脚本缓存中,执行EVALSHA命令时Redis会根据提供的摘要从脚本缓存中查找对应的脚本内容,找到则执行脚本,否则返回错误:“NO SCRIPT No matching script. Please use EVAL.”
命令参数:EVALSHA sha1 numkeys key [key ...] arg [arg ...]
sha1是通过SCRIPT LOAD
生成的SHA1校验码。
对比
eval命令会将脚本添加到脚本缓存中,并立即对输入的脚本进行求值。
evalsha命令会将脚本添加到脚本缓存中,但并不立即执行这个脚本。
SCRIPT LOAD
参考:script-load文档
将脚本加入缓存,但不执行, 返回脚本的SHA1摘要。
SCRIPT EXISTS
参考:script-exists文档
判断脚本是否已被缓存。
SCRIPT FLUSH
参考:script-flush文档
清空脚本缓存,redis将脚本的SHA1摘要加入到脚本缓存后会永久保留,手动使用SCRIPT FLUSH命令清空脚本缓存。
SCRIPT KILL
参考:script-kill文档
强制终止当前脚本的执行。但是如果当前执行的脚步对redis的数据进行写操作,则SCRIPT KILL命令不会终止脚本的运行,以防止脚本只执行一部分。脚本中的所有命令,要么都执行,要么都不执行。
SCRIPT DEBUG
编码实例
Jedis集成Lua
将上面演示的eval命令翻译成基于Jedis的Java代码:
@Test
public void testLuaWithJedis() {
Jedis jedis = new Jedis("10.114.31.113", 6408);
String luaStr = "return {KEYS[1],ARGV[1]}";
Object result = jedis.eval(luaStr, Lists.newArrayList("testKey"), Lists.newArrayList("testValue"));
}
但是报错。
JedisMovedDataException: MOVED 165 10.114.31.113:6407
解决方法:
Spring Boot中使用Lua脚本
spring-boot-starter-data-redis依赖,使用redisTemplate
lua脚本中的变量都要是local 的,不可以是全局变量,否则会报错。详见 http://doc.redisfans.com/script/eval.html#id6
使用DefaultRedisScript加载lua脚本
在应用上下文中配置一个DefaultRedisScript单例,避免在每个脚本执行的时候重复创建脚本的SHA1:
@Bean
public DefaultRedisScript<Boolean> redisScript() {
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/checkandset.lua")));
redisScript.setResultType(Boolean.class);
return redisScript;
}
参考
redis-lua-script
redis-cross-slot-error
https://blog.csdn.net/u011943534/article/details/82717253
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
2020-03-23 一文总结HTTP缓存