Redis使用Lua脚本详解
前言本文将讲解 RedisLua脚本的基本操作以及与 Java项目的集成使用。Lua脚本Lua是一个高效的轻量级脚本语言,在葡萄牙语中是“月亮”的意思,用标准C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。为什么要在程序中嵌入 Lua脚本?它解决了什么问题?假设要开发一个 iPhone 的电子宠物游戏,设定玩家每次给宠物喂食,宠物饥饿值就会减
前言
本文将讲解 Redis
Lua脚本的基本操作以及与 Java
项目的集成使用。
Lua脚本
Lua
是一个高效的轻量级脚本语言,在葡萄牙语中是“月亮”的意思,用标准C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
为什么要在程序中嵌入 Lua
脚本?它解决了什么问题?
假设要开发一个 iPhone 的电子宠物游戏,设定玩家每次给宠物喂食,宠物饥饿值就会减 N 点,这个 N 如果设置为一个定值,以后需要更改的话,玩家就需要到 App Store 中升级游戏,随着需求的变化和扩展,每次修改完都需要玩家进行应用升级,现在有一个更好的办法——嵌入Lua脚本实现需求的更改,将业务逻辑写到 Lua脚本
中,在你的应用程序中添加一个 Lua
解释器,每次需要执行 Lua脚本
的逻辑时,通过解释器调用这个 Lua脚本
,下次再需要调整算法时,只要从网上更新这个脚本就好了,连 游戏都不用重启。总之越多的可变的逻辑放到脚本上,你的程序升级或扩展越容易。
其实 Redis
和电子宠物游戏遇到的问题相似,很多人希望在 Redis
中加入各种各样的命令,这样命令中有的确实很实用,但却可以使用多个 Redis
已有的命令去实现,Redis
中不可能包含所有开发者需要的命令,所以在 Redis 2.6
版本中提供了 Lua脚本
功能让开发者扩展 Redis
Redis中使用 Lua 的好处
- 减少网络开销,在
Lua脚本
中可以把多个命令放在同一个脚本中运行 - 原子操作,
Redis
会将整个脚本作为一个整体执行,中间不会被其他命令插入(编写脚本过程中无需担心会出现竞态条件) - 复用性,客户端发送的脚本会永远存储在
Redis
中,意味着其他客户端可以复用这一脚本
Redis + Lua
从 Redis2.6
开始, Eval
命令使用内置的Lua
解释器执行脚本 ,不需要单独安装 Lua
EVAL
语法
Redis Eval 命令基本语法如下
redis 127.0.0.1:6379> EVAL script numkeys key [key ...] arg [arg ...]
参数说明:
- script: 参数是一段 Lua 5.1 脚本程序。脚本不必(也不应该)定义为一个 Lua 函数。
- numkeys: 用于指定键名参数的个数。
- key [key ...] : 从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 1 为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。
- arg [arg ...] : 附加参数,在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)
每次执行 Eval
命令时 Redis
都会将脚本的 SHA1 摘要加入到脚本缓存中,以便下次客户端可以使用 EVALSHA
命令调用该脚本。
实例
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 username age jack 20
1) "username"
2) "age"
3) "jack"
4) "20"
SCRIPT LOAD
如果只是希望将脚本加入脚本缓存而不执行则可以使用 SCRIPT LOAD
命令,返回值是脚本的 SHA1 摘要。
127.0.0.1:6379> script load "return 1"
"e0e1f9fabfc9d4800c877a703b823ac0578ff8db"
SCRIPT EXISTS
如判断脚本是否已经被缓存,使用 SCRIPT EXISTS
命令查看
127.0.0.1:6379> script exists e0e1f9fabfc9d4800c877a703b823ac0578ff8db
1) (integer) 1
SCRIPT FLUSH
清空脚本缓存
127.0.0.1:6379> script flush
OK
SCRIPT KILL
强制终止当前脚本的执行,可使用 SCRIPT KILL
命令
SpringBoot 集成 Redis 调用 Lua脚本
1、resources
下定义 redis.lua
-- 调用 redis 中的 key 值
local current = redis.call('GET', KEYS[1])
-- 判断key值是否与传的key值相等
if current == ARGV[1]
-- 若相等,将value置换为第二个arg
then redis.call('SET', KEYS[1], ARGV[2])
return true
end
return false
2、注入 DefaultRedisScript
@Configuration
public class RedisUtil {
@Bean
public DefaultRedisScript<Boolean> redisScript() {
DefaultRedisScript<Boolean> objectDefaultRedisScript = new DefaultRedisScript<>();
objectDefaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/redis.lua")));
objectDefaultRedisScript.setResultType(Boolean.class);
return objectDefaultRedisScript;
3、调用 lua
脚本
@Test
public void testScript() {
String key = "testredislua";
// set testredislua feiyangyang
template.opsForValue().set(key, "feiyangyang");
String str = template.opsForValue().get(key);
System.out.println(str);
// 执行lua脚本,传 key 和 两个 args
template.execute(redisScript, Collections.singletonList(key), "feiyangyang", "123");
str = template.opsForValue().get(key);
System.out.println(str);
}
4、打印结果
feiyangyang
123
Lua 的原子性与执行时间
Redis
脚本的执行是原子的,即脚本执行期间 Redis
不会执行其他命令,所有命令都必须等脚本执行完后才能执行,为防止某个脚本执行时间过长导致Redis
无法提供服务,Redis
提供了 lua-time-limit
参数限制脚本的最长运行时间,默认为 5s,当脚本执行时间超过这一限制后,Redis
将开始接受其他命令但不会执行,而是会返回 BUSY
错误。
如现有两个 redis-cli 实例 a 和 b ,在 a 中执行一个死循环脚本,再在 b 中执行一条命令,这时实例 b 的命令不会马上返回结果,因为 Redis
已经被实例 a 发送的死循环脚本阻塞了,无法执行其他命令,等脚本执行 5s 后实例 b 收到了 BUSY
错误。
虽然此时 Redis
可以接受任何命令,但实际会执行的只要两个命令:SCRIPT KILL
和 SHUTDOWN NOSAVE
,在实例 b 中执行 SCRIPT KILL
命令可以终止当前脚本的运行。终止后实例 a 中就会返回错误。
小结
由于 Redis
脚本非常高效,所以大部分情况下不用担心脚本的性能,但同时由于脚本的强大功能,很多原本在程序中执行的逻辑都可以放到脚本中执行,这时就需要开发者根据具体应用权衡到底哪些任务适合交给脚本。通常来讲不应该在脚本中进行大量耗时的计算,因为毕竟 Redis
是单线程执行脚本,而程序却能够多线程执行。
更多推荐
所有评论(0)