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”错误。

优势

  1. 减少网络开销:可以将多个请求通过脚本的形式一次发送,减少网络时延和请求次数
  2. 原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。无需担心会出现竞态条件,无需使用事务
  3. 代码复用:客户端发送的脚本会永久存在Redis中,其他客户端可复用
  4. 速度快:JIT编译器可以显著地提高性能
  5. 可移植:Lua基于C,只要是有ANSI C 编译器的平台都可以编译,甚至浏览器也可以完美使用(翻译成JS)
  6. 源码小巧:2w行C代码,可以编译进182K的可执行文件,加载快,运行快

命令

Redis支持Lua脚本功能,随之新增的几个命令(script debug除外,是Redis 3.2版本引入的命令)。

eval

参考:eval文档
命令参数:EVAL script numkeys key [key ...] arg [arg ...]
命令解读:

  1. script参数是一段Lua脚本程序,会被运行在Redis服务器上下文中,这段脚本不必(也不应该)定义为一个Lua函数
  2. numkeys参数指定键名参数的个数;当脚本不需要任何参数时,也不能省略这个参数(设为0)
  3. 键名参数key [key ...],表示在脚本中所用到的那些Redis键(key),可在Lua中通过全局变量KEYS数组,用1为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)
  4. 附加参数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

参考: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

你可能感兴趣的:(Redis)