Lua是一个高效的轻量级脚本语言(和JavaScript类似),用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Lua在葡萄牙语中是“月亮”的意思,它的logo形式卫星,寓意是Lua是一个“卫星语言”,能够方便地嵌入到其他语言中使用;其实在很多常见的框架中,都有嵌入Lua脚本的功能,比如OpenResty、Redis等。
使用Lua脚本的好处:
减少网络开销,在Lua脚本中可以把多个命令放在同一个脚本中运行
原子操作,redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。换句话说,编写脚本的过程中无需担心会出现竞态条件
复用性,客户端发送的脚本会永远存储在redis中,这意味着其他客户端可以复用这一脚本来完成同样的逻辑
Lua是一个独立的脚本语言,所以它有专门的编译执行工具,下面简单带大家安装一下。
下载Lua源码包: Lua: download
https://www.lua.org/ftp/lua-5.4.3.tar.gz
安装步骤
tar -zxvf lua-5.4.3.tar.gz cd lua-5.4.3 make linux make install
最后,直接输入lua
命令即可进入lua的控制台。Lua脚本有自己的语法、变量、逻辑运算符、函数等,这块我就不在这里做过多的说明,可以自己进入下面链接进去看
https://www.runoob.com/lua/lua-tutorial.html
Redis中集成了Lua的编译和执行器,所以我们可以在Redis中定义Lua脚本去执行。同时,在Lua脚本中,可以直接调用Redis的命令,来操作Redis中的数据。
redis.call(‘set’,'hello','world') local value=redis.call(‘get’,’hello’)
redis.call 函数的返回值就是redis命令的执行结果,前面我们介绍过redis的5中类型的数据返回的值的类型也都不一样,redis.call函数会将这5种类型的返回值转化对应的Lua的数据类型
在很多情况下我们都需要脚本可以有返回值,毕竟这个脚本也是一个我们所编写的命令集,我们可以像调用其他redis内置命令一样调用我们自己写的脚本,所以同样redis会自动将脚本返回值的Lua数据类型转化为Redis的返回值类型。 在脚本中可以使用return 语句将值返回给redis客户端,通过return语句来执行,如果没有执行return,默认返回为nil。
编写完脚本后最重要的就是在程序中执行脚本。Redis提供了EVAL命令可以使开发者像调用其他Redis内置命令一样调用脚本。
[EVAL] [脚本内容] [key参数的数量] [key …] [arg …]
可以通过key和arg这两个参数向脚本中传递数据,他们的值可以在脚本中分别使用KEYS和ARGV 这两个类型的全局变量访问。
比如我们通过脚本实现一个set命令,通过在redis客户端中调用,那么执行的语句是:
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 lua hello
上述脚本相当于使用Lua脚本调用了Redis的set
命令,存储了一个key=lua,value=hello到Redis中。
考虑到我们通过eval执行lua脚本,脚本比较长的情况下,每次调用脚本都需要把整个脚本传给redis,比较占用带宽。为了解决这个问题,redis提供了EVALSHA命令允许开发者通过脚本内容的SHA1摘要来执行脚本。该命令的用法和EVAL一样,只不过是将脚本内容替换成脚本内容的SHA1摘要
Redis在执行EVAL命令时会计算脚本的SHA1摘要并记录在脚本缓存中
执行EVALSHA命令时Redis会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果找到了就执行脚本,否则返回“NOSCRIPT No matching script,Please use EVAL”
# 将脚本加入缓存并生成sha1命令 script load "return redis.call('get','lua')" # ["13bd040587b891aedc00a72458cbf8588a27df90"] # 传递sha1的值来执行该命令 evalsha "13bd040587b891aedc00a72458cbf8588a27df90" 0
通过lua脚本来实现一个访问频率限制功能。
思路,定义一个key,key中包含ip地址。 value为指定时间内的访问次数,比如说是10秒内只能访问3次。
定义Lua脚本
local times=redis.call('incr',KEYS[1]) -- 如果是第一次进来,设置一个过期时间 if times == 1 then redis.call('expire',KEYS[1],ARGV[1]) end -- 如果在指定时间内访问次数大于指定次数,则返回0,表示访问被限制 if times > tonumber(ARGV[2]) then return 0 end -- 返回1,允许被访问 return 1
定义controller,提供访问测试方法
@RestController public class RedissonLuaController { @Autowired RedissonClient redissonClient; private final String LIMIT_LUA="local times=redis.call('incr',KEYS[1])\n" + "if times==1 then\n" + " redis.call('expire',KEYS[1],ARGV[1])\n" + "end\n" + "if times > tonumber(ARGV[2]) then\n" + " return 0\n" + "end \n" + "return 1"; @GetMapping("/lua/{id}") public String lua(@PathVariable("id") Integer id) throws ExecutionException, InterruptedException { RScript rScript=redissonClient.getScript(); List<Object> keys= Arrays.asList("LIMIT:"+id); RFuture<Object> future=rScript.evalAsync(RScript.Mode.READ_WRITE,LIMIT_LUA, RScript.ReturnType.INTEGER,keys,10,3); return future.get().toString(); } }
要注意,上述脚本执行的时候会有问题,因为redis默认的序列化方式导致value的值在传递到脚本中时,转成了对象类型,需要修改redisson.yml
文件,增加codec的序列化方式。
application.yml
spring: redis: redisson: file: classpath:redisson.yml
redisson.yml
singleServerConfig: address: redis://192.168.221.128:6379 codec: !<org.redisson.codec.JsonJacksonCodec> {}
redis的脚本执行是原子的,即脚本执行期间Redis不会执行其他命令。所有的命令必须等待脚本执行完以后才能执行。为了防止某个脚本执行时间过程导致Redis无法提供服务。Redis提供了lua-time-limit参数限制脚本的最长运行时间。默认是5秒钟。
非事务性操作
当脚本运行时间超过这个限制后,Redis将开始接受其他命令但不会执行(以确保脚本的原子性),而是返回BUSY的错误,下面演示一下这种情况。
打开两个客户端窗口,在第一个窗口中执行lua脚本的死循环
eval "while true do end" 0
在第二个窗口中运行get lua
,会得到如下的异常。
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
我们会发现执行结果是Busy, 接着我们通过script kill 的命令终止当前执行的脚本,第二个窗口的显示又恢复正常了。
存在事务性操作
如果当前执行的Lua脚本对Redis的数据进行了修改(SET、DEL等),那么通过SCRIPT KILL 命令是不能终止脚本运行的,因为要保证脚本运行的原子性,如果脚本执行了一部分终止,那就违背了脚本原子性的要求。最终要保证脚本要么都执行,要么都不执行
同样打开两个窗口,第一个窗口运行如下命令
eval "redis.call('set','name','ljx') while true do end" 0
在第二个窗口运行
get lua
结果一样,仍然是busy,但是这个时候通过script kill命令,会发现报错,没办法kill。遇到这种情况,只能通过shutdown nosave命令来强行终止redis。shutdown nosave和shutdown的区别在于 shutdown nosave不会进行持久化操作,意味着发生在上一次快照后的数据库修改都会丢失。