Lua是什么

Lua 是一个小巧的脚本语言。是巴西里约热内卢天主教大学(Pontifical Catholic University of Rio de Janeiro)里的一个研究小组,由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo所组成并于1993年开发。 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Lua由标准C编写而成,几乎在所有操作系统和平台上都可以编译,运行。Lua并没有提供强大的库,这是由它的定位决定的。所以Lua不适合作为开发独立应用程序的语言。Lua 有一个同时进行的JIT项目,提供在特定平台上的即时编译功能。

Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,这使得Lua在应用程序中可以被广泛应用。不仅仅作为扩展脚本,也可以作为普通的配置文件,代替XML,ini等文件格式,并且更容易理解和维护. Lua由标准C编写而成,代码简洁优美,几乎在所有操作系统和平台上都可以编译,运行。一个完整的Lua解释器不过200k,在目前所有脚本引擎中,Lua的速度是最快的。这一切都决定了Lua是作为嵌入式脚本的最佳选择。

基本知识

参考文档

Lua 数据类型

ua是动态类型语言,变量不要类型定义,只需要为变量赋值。 值可以存储在变量中,作为参数传递或结果返回。

Lua中有8个基本类型分别为:nil、boolean、number、string、userdata、function、thread和table。

易语言和lua哪个性能好_数据库

Lua 变量

变量在使用前,必须在代码中进行声明,即创建该变量。编译程序执行代码之前编译器需要知道如何给语句变量开辟存储区,用于存储变量的值。

Lua 变量有三种类型:全局变量、局部变量、表中的域。

函数外的变量默认为全局变量,除非用 local 显示声明。函数内变量与函数的参数默认为局部变量。

局部变量的作用域为从声明位置开始到所在语句块结束(或者是直到下一个同名局部变量的声明)。

变量的默认值均为 nil。

Lua 流程控制

Lua 提供了以下控制结构语句:

易语言和lua哪个性能好_java_02

if(0)
then
    print("0 为真")
end

Lua 函数

在Lua中,函数是对语句和表达式进行抽象的主要方法。既可以用来处理一些特殊的工作,也可以用来计算一些值。

Lua 提供了许多的内建函数,你可以很方便的在程序中调用它们,如print()函数可以将传入的参数打印在控制台上。

Lua 函数主要有两种用途:

  • 完成指定的任务,这种情况下函数作为调用语句使用;
  • 计算并返回值,这种情况下函数作为赋值语句的表达式使用。

Lua 编程语言函数定义格式如下:

optional_function_scope function function_name( argument1, argument2, argument3..., argumentn)
   function_body
 return result_params_comma_separated
end

Lua 运算符

运算符是一个特殊的符号,用于告诉解释器执行特定的数学或逻辑运算。Lua提供了以下几种运算符类型:

  • 算术运算符
  • 关系运算符
  • 逻辑运算符
  • 其他运算符

Redis脚本

Redis 2.6及更高版本通过eval和evalsha命令提供了对运行Lua脚本的支持。

Lua 嵌入 Redis 优势:

  • 减少网络开销: 不使用 Lua 的代码需要向 Redis 发送多次请求, 而脚本只需一次即可, 减少网络传输;
  • 原子操作: Redis 将整个脚本作为一个原子执行, 无需担心并发, 也就无需事务;
  • 复用: 脚本会永久保存 Redis 中, 其他客户端可继续使用.

命令行运行脚本

命令

EVAL script numkeys key [key …] arg [arg …]

EVAL 和 EVALSHA 命令是从 Redis 2.6.0 版本开始的,使用内置的 Lua 解释器,可以对 Lua 脚本进行求值。

EVAL的第一个参数是一段 Lua 5.1 脚本程序。 这段Lua脚本不需要(也不应该)定义函数。它运行在 Redis 服务器中。

EVAL的第二个参数是参数的个数,后面的参数(从第三个参数),表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 1 为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。

在命令的最后,那些不是键名参数的附加参数 arg [arg …] ,可以在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。

常用Lua函数:

  • redis.call()
  • redis.pcall()

redis.call() 与 redis.pcall()很类似, 他们唯一的区别是当redis命令执行结果返回错误时, redis.call()将返回给调用者一个错误,而redis.pcall()会将捕获的错误以Lua表的形式返回。

# 这段脚本的确实现了将键 foo 的值设为 bar 的目的,但是,它违反了 EVAL 命令的语义,因为脚本里使用的所有键都应该由 KEYS 数组来传递
127.0.0.1:6379> eval "return redis.call('set','foo','bar')" 0
OK
# 正确方式
127.0.0.1:6379> eval "return redis.call('set',KEYS[1],'bar')" 1 foo
OK

要求使用正确的形式来传递键(key)是有原因的,因为不仅仅是 EVAL 这个命令,所有的 Redis 命令,在执行之前都会被分析,籍此来确定命令会对哪些键进行操作。

因此,对于 EVAL 命令来说,必须使用正确的形式来传递键,才能确保分析工作正确地执行。 除此之外,使用正确的形式来传递键还有很多其他好处,它的一个特别重要的用途就是确保 Redis 集群可以将你的请求发送到正确的集群节点。 (对 Redis 集群的工作还在进行当中,但是脚本功能被设计成可以与集群功能保持兼容。)不过,这条规矩并不是强制性的, 从而使得用户有机会滥用(abuse) Redis 单实例配置(single instance configuration),代价是这样写出的脚本不能被 Redis 集群所兼容。

Lua 脚本能返回一个值,这个值能按照一组转换规则从Lua转换成redis的返回类型。

Lua 数据类型和 Redis 数据类型之间转换

当 Lua 通过 call() 或 pcall() 函数执行 Redis 命令的时候,命令的返回值会被转换成 Lua 数据结构。 同样地,当 Lua 脚本在 Redis 内置的解释器里运行时,Lua 脚本的返回值也会被转换成 Redis 协议(protocol),然后由 EVAL 将值返回给客户端。

数据类型之间的转换遵循这样一个设计原则:如果将一个 Redis 值转换成 Lua 值,之后再将转换所得的 Lua 值转换回 Redis 值,那么这个转换所得的 Redis 值应该和最初时的 Redis 值一样。

换句话说, Lua 类型和 Redis 类型之间存在着一一对应的转换关系。

Redis 到 Lua 的转换表:

  • Redis integer reply -> Lua number / Redis 整数转换成 Lua 数字
  • Redis bulk reply -> Lua string / Redis bulk 回复转换成 Lua 字符串
  • Redis multi bulk reply -> Lua table (may have other Redis data types nested) / Redis 多条 bulk 回复转换成 Lua 表,表内可能有其他别的 Redis 数据类型
  • Redis status reply -> Lua table with a single ok field containing the status / Redis 状态回复转换成 Lua 表,表内的 ok 域包含了状态信息
  • Redis error reply -> Lua table with a single err field containing the error / Redis 错误回复转换成 Lua 表,表内的 err 域包含了错误信息
  • Redis Nil bulk reply and Nil multi bulk reply -> Lua false boolean type / Redis 的 Nil 回复和 Nil 多条回复转换成 Lua 的布尔值 false

Lua 到 Redis 的转换表:

  • Lua number -> Redis integer reply (the number is converted into an integer) / Lua 数字转换成 Redis 整数
  • Lua string -> Redis bulk reply / Lua 字符串转换成 Redis bulk 回复
  • Lua table (array) -> Redis multi bulk reply (truncated to the first nil inside the Lua array if any) / Lua 表(数组)转换成 Redis 多条 bulk 回复
  • Lua table with a single ok field -> Redis status reply / 一个带单个 ok 域的 Lua 表,转换成 Redis 状态回复
  • Lua table with a single err field -> Redis error reply / 一个带单个 err 域的 Lua 表,转换成 Redis 错误回复
  • Lua boolean false -> Redis Nil bulk reply. / Lua 的布尔值 false 转换成 Redis 的 Nil bulk 回复
  • 从 Lua 转换到 Redis 有一条额外的规则,这条规则没有和它对应的从 Redis 转换到 Lua 的规则:
  • Lua boolean true -> Redis integer reply with value of 1. / Lua 布尔值 true 转换成 Redis 整数回复中的 1

还有下面两点需要重点注意:

  • lua中整数和浮点数之间没有什么区别。因此,我们始终Lua的数字转换成整数的回复,这样将舍去小数部分。如果你想从Lua返回一个浮点数,你应该将它作为一个字符串(见比如ZSCORE命令)。
  • There is no simple way to have nils inside Lua arrays, this is a result of Lua table semantics, so when Redis converts a Lua array into Redis protocol the conversion is stopped if a nil is encountered.
脚本的原子性

Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行: 当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。 这和使用 MULTI / EXEC 包围的事务很类似。 在其他别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)。 另一方面,这也意味着,执行一个运行缓慢的脚本并不是一个好主意。写一个跑得很快很顺溜的脚本并不难, 因为脚本的运行开销(overhead)非常少,但是当你不得不使用一些跑得比较慢的脚本时,请小心, 因为当这些蜗牛脚本在慢吞吞地运行的时候,其他客户端会因为服务器正忙而无法执行命令。

RedisTemplate运行脚本

Spring Data Redis为正在运行的脚本提供了高级抽象,该脚本处理序列化并自动使用Redis脚本缓存。

脚本可以通过调用运行execute的方法RedisTemplate和ReactiveRedisTemplate。两者都使用可配置的ScriptExecutor(或ReactiveScriptExecutor)来运行提供的脚本。默认情况下,ScriptExecutor(或ReactiveScriptExecutor)负责序列化提供的键和参数并反序列化脚本结果。这是通过模板的键和值序列化程序完成的。还有一个额外的重载,可让您传递脚本参数和结果的自定义序列化程序。

默认值ScriptExecutor通过检索脚本的SHA1并首先尝试运行来优化性能,如果脚本未在Redis脚本缓存中存在,则evalsha返回默认值eval。

以下示例通过使用Lua脚本运行常见的“检查并设置”方案。这是Redis脚本的理想用例,因为它需要原子地运行一组命令,并且一个命令的行为会受到另一个命令的结果的影响。

核心类

RedisTemplate提供了两个execute执行Lua脚本:

// 传入脚本对象、Key队列、多个参数
	// execute没有传入序列化器时,默认使用的是RedisTemplate的ValueSerializer序列化器
    @Nullable
    <T> T execute(RedisScript<T> var1, List<K> var2, Object... var3);
	// 传入脚本对象、Key队列、Key和参数序列化器、返回值序列化器、多个参数
    @Nullable
    <T> T execute(RedisScript<T> var1, RedisSerializer<?> var2, RedisSerializer<T> var3, List<K> var4, Object... var5);

首先需要创建一个RedisScript脚本对象DefaultRedisScript,核心代码如下:

public class DefaultRedisScript<T> implements RedisScript<T>, InitializingBean {
	// synchronized锁对象,通过文件创建的脚本,每次执行时,都会加锁,判断文件是否被修改
	// 可以自己实现ScriptSource对象及方法,去掉synchronized,提高性能
    private final Object shaModifiedMonitor;
    @Nullable
    // 脚本源对象,实现类为ResourceScriptSource,可以通过文件加载脚本
    private ScriptSource scriptSource;
    @Nullable
    private String sha1;
    @Nullable
    // 脚本执行返回结果类型
    private Class<T> resultType;
    // 构造方法,传入脚本及返回结果类型,设置为Java中的数据类型即可,会转换
    public DefaultRedisScript(String script, @Nullable Class<T> resultType) {
        this.shaModifiedMonitor = new Object();
        this.setScriptText(script);
        this.resultType = resultType;
    }
    //  获取脚本源对象中的脚本,会加锁
    pulic String getScriptAsString() {
        try {
            return this.scriptSource.getScriptAsString();
        } catch (IOException var2) {
            throw new ScriptingException("Error reading script text", var2);
        }
    }

最终的execute方法

public <T> T execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, RedisSerializer<T> resultSerializer, List<K> keys, Object... args) {
        return this.template.execute((connection) -> {
            // script.getResultType(),获取脚本对象设置的返回结果类型
            // 将JAVA中的类型转为Lua脚本中的数据类型
            ReturnType returnType = ReturnType.fromJavaType(script.getResultType());
            // 将key和参数序列化为二进制
            byte[][] keysAndArgs = this.keysAndArgs(argsSerializer, keys, args);
            int keySize = keys != null ? keys.size() : 0;
            if (!connection.isPipelined() && !connection.isQueueing()) {
                // 执行脚本
                return this.eval(connection, script, returnType, keySize, keysAndArgs, resultSerializer);
            } else {
                connection.eval(this.scriptBytes(script), returnType, keySize, keysAndArgs);
                return null;
            }
        });
    }

案例演示

  1. resources/META-INF/scripts目录下创建脚本文件checkandset.lua
-- checkandset.lua
-- 获取一个Key的值
local current = redis.call('GET', KEYS[1])
-- 如果这个值等于传入的第一个参数
if current == ARGV[1]
  -- 设置这个Key的值为第二个参数
  then redis.call('SET', KEYS[1], ARGV[2])
  return true
end
-- 如果这个值不等于传入的第一个参数,直接返回false
return false

易语言和lua哪个性能好_redis_03

  1. 使用redisTemplate执行脚本
@Test
    void luaTest() throws IOException {
        redisTemplate.opsForValue().set("lua:key", "aaa");
        // 根据脚本文件位置创建ScriptSource对象
        ScriptSource scriptSource = new ResourceScriptSource(new ClassPathResource("META-INF/scripts/checkandset.lua"));
        // 根据脚本和返回值类型创建DefaultRedisScript对象,泛型定义为返回值类型
        DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>(scriptSource.getScriptAsString(), Boolean.class);
        // 执行脚本
        ArrayList<String> keys = Lists.newArrayList("lua:key");
        Boolean result = redisTemplate.execute(redisScript, keys, "aaa", "bbb");
        System.out.println(result);
    }