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 变量有三种类型:全局变量、局部变量、表中的域。
函数外的变量默认为全局变量,除非用 local 显示声明。函数内变量与函数的参数默认为局部变量。
局部变量的作用域为从声明位置开始到所在语句块结束(或者是直到下一个同名局部变量的声明)。
变量的默认值均为 nil。
Lua 流程控制
Lua 提供了以下控制结构语句:
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;
}
});
}
案例演示:
- 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
- 使用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);
}