赞
踩
Lua是一种轻量级脚本语言,它是用C语言编写的,跟数据库的存储过程有点类似。
在Redis从2.6版本之后,开始引入Lua脚本,也就是说,Redis可以用Lua来执行Redis命令。
官方文档:
https://redis.io/commands/eval/
使用Lua脚本来执行Redis命令的好处:
原子性
。Lua语法请自行学习,推荐菜鸟教程简单入门:https://www.runoob.com/lua/lua-tutorial.html
使用eval命令,可以调用Lua,语法格式:
eval lua-script key-num [key1 key2 key3 ...] [value1 value2 value3 ...]
实例:返回一个字符串,0个参数:
127.0.0.1:6379> eval "return 'Hello World'" 0
"Hello World"
Redis中的Lua脚本真正的用途是用来执行Redis命令。也就是说,Redis中运行Lua脚本,用Lua脚本来执行Redis命令。
语法格式:
redis.call(command, key [param1, param2 ...])
# 使用示例
127.0.0.1:6379> eval "return redis.call('set', 'zhangsan', '123')" 0
OK
127.0.0.1:6379> get zhangsan
"123"
# 上面的值是写死的,我们可以使用传参的方式:
127.0.0.1:6379> eval "return redis.call('set', KEYS[1], ARGV[1])" 1 lisi 321
OK
127.0.0.1:6379> get lisi
"321"
# 如果KEY和ARGV有多个,继续往后追加即可
127.0.0.1:6379> eval "return redis.call('hset', KEYS[1], ARGV[1], ARGV[2])" 1 wangwu today run
(integer) 1
127.0.0.1:6379> hget wangwu today
"run"
在redis-cli中直接写Lua脚本不够方便,也不能实现编辑和复用,通常我们会把Lua脚本放在文件里面,然后执行这个文件。
创建一个Lua脚本文件:
# my.lua:
redis.call('hset', KEYS[1], ARGV[1], ARGV[2])
return redis.call('hget', KEYS[1], ARGV[1])
# 执行结果
>redis-cli --eval my.lua 1 shandong , qingdao shinan
"shinan"
划重点! keys...与values...之间用逗号隔开,逗号两侧必须有空格!
# my.lua
redis.call('hset', KEYS[1], ARGV[1], ARGV[2])
redis.call('hset', KEYS[1], ARGV[3], ARGV[4])
redis.call('set', KEYS[2], ARGV[5])
return redis.call('hget', KEYS[1], ARGV[1])..redis.call('hget', KEYS[1], ARGV[3])..redis.call('get', KEYS[2])
# 执行结果
D:\Redis-x64-3.2.100>redis-cli --eval my.lua 2 k1 k2 , v1 v2 v3 v4 v5
"v2v4v5"
Lua脚本比较长的情况下, 如果每次调用脚本都需要把整个脚本传给Redis服务端,会产生比较大的网络开销。为了解决这个问题,Redis可以缓存Lua脚本并生成SHA1摘要码,后面可以直接通过摘要码来执行Lua脚本。
缓存Lua脚本涉及两个命令,首先是在服务端缓存lua脚本生成一个摘要码,用script load命令。
# 缓存
127.0.0.1:6379> script load "return 'hello world'"
"5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
# 调用
127.0.0.1:6379> evalsha "5332031c6b470dc5a0dd9b4bf2030dea6d65de91" 0
"hello world"
我们实现一个自乘运算,让它乘以后面的参数:
# 定义一个lua local curVal = redis.call("get", KEYS[1]) if curVal == false then curVal = 1 else curVal = tonumber(curVal) end curVal = curVal * tonumber(ARGV[1]) redis.call("set", KEYS[1], curVal) return curVal # 将lua合并为一行,语句间使用分号分割,缓存起来 script load 'local curVal = redis.call("get", KEYS[1]); if curVal == false then curVal = 1 else curVal = tonumber(curVal) end; curVal = curVal * tonumber(ARGV[1]); redis.call("set", KEYS[1], curVal); return curVal' "2c9150e0a662c40d6766476fc288e61beb2344d0" # 执行 127.0.0.1:6379> evalsha "2c9150e0a662c40d6766476fc288e61beb2344d0" 1 num 6 (integer) 6 127.0.0.1:6379> evalsha "2c9150e0a662c40d6766476fc288e61beb2344d0" 1 num 6 (integer) 36
Redis的指令执行本身是单线程的,这个线程还要执行客户端的Lua脚本,如果Lua脚本执行超时或者陷入了死循环,是不是就没办法为客户端提供服务了?
# 客户端1,执行死循环
127.0.0.1:6379> eval 'while(true) do end' 0
# 客户端2,无法再执行命令,即使客户端1强制退出也无法执行命令
127.0.0.1:6379> get tom
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
我们发现,脚本执行时间过长,其他命令都会执行失败。
脚本执行有一个超时时间的设置,默认为5秒钟。
lua-time-limit 5000
超过5秒钟,其他客户端的命令不会等待,而是直接返回“BUSY”错误。
我们可以通过script kill命令,终止脚本的执行:
# 客户端1
127.0.0.1:6379> eval 'while(true) do end' 0
(error) ERR Error running script (call to f_eec1f08dafc6bfdf256e3820d971514a3a24267e): @user_script:1: Script killed by user with SCRIPT KILL...
(13.10s)
# 客户端2执行script kill
127.0.0.1:6379> script kill
OK
但是要注意的是,并不是所有的lua脚本执行的时候都可以kill,如果当前执行的Lua脚本对Redis的数据进行了修改(Set、DEL等),那么通过script kill命令是不能终止脚本运行的。
# 客户端1
127.0.0.1:6379> eval "redis.call('set', 'lua','666') while(true) do end" 0
# 客户端2
127.0.0.1:6379> script kill
(error) UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.
这个时候执行script kill会返回UNKILLABLE错误。为什么要这样设计?为什么包含修改的脚本不能中断?因为要保证脚本运行的原子性。如果脚本执行了一部分被终止,就违背了脚本的原子性的目的。
遇到这钟情况,只能通过shutdown nosave命令,直接把Redis服务停掉。
127.0.0.1:6379> shutdown nosave
not connected>
正常关机是shutdown。shutdown nosave 和shutdown 的区别就在于,shutdown nosave不会进行持久化操作,意味着发生在上一次快照后的数据库修改都会丢失。
所以,我们有一些特殊需求的话,可以用Lua实现,但是要注意那些耗时的操作。
springboot使用redisTemplate操作lua脚本
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。