前言

本文将讲解 RedisLua脚本的基本操作以及与 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 KILLSHUTDOWN NOSAVE ,在实例 b 中执行 SCRIPT KILL 命令可以终止当前脚本的运行。终止后实例 a 中就会返回错误。

小结

由于 Redis 脚本非常高效,所以大部分情况下不用担心脚本的性能,但同时由于脚本的强大功能,很多原本在程序中执行的逻辑都可以放到脚本中执行,这时就需要开发者根据具体应用权衡到底哪些任务适合交给脚本。通常来讲不应该在脚本中进行大量耗时的计算,因为毕竟 Redis 是单线程执行脚本,而程序却能够多线程执行。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐