1 介绍
Redis自2.6.0加入了Lua脚本相关的命令,EVAL, EVALSHA, SCRIPT EXISTS, SCRIPT FLUSH, SCRIPT KILL, SCRIPT LOAD,自3.2.0加入了Lua脚本的调试功能和命令。
Lua脚本可以运行在任何平台上,也可以嵌入到大多数语言中,来扩展其功能。Lua脚本是用C语言写的,体积很小,运行速度很快。
使用Redis Lua脚本功能,用户可以向服务器发送Lua脚本来执行自定义动作,获取脚本的相应数据。Redis服务器会单线程原子性执行Lua脚本,保证Lua脚本在执行过程中不会被任意其他请求打断。
生产环境中,推荐使用EVALSHA,相较于EVAL的每次发送脚本主体、占用带宽,EVALSHA会更高效。
使用Lua脚本的好处:
1) 减少网络开销:将脚本发送到服务端,在服务端进行计算,并将结果返回客户端,避免了传递大量数据。
2) 原子性的操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入,因此在编写脚本的过程中,无需使用事物
3) 代码复用
使用Lua脚本需注意的问题:
1) 单线程执行。所有Lua命令都在同一个Lua解释器中执行,当一个脚本执行时,其他脚本或Redis命令都不能执行。如果脚本执行慢,会比较麻烦。
2) 写纯函数脚本
3) Redis集群模式要求单个Lua脚本操作的Key必须在同一节点上,但是Cluster会将数据自动分布到不同的节点(虚拟的16384个slot)。阿里云集群版官网也有说明:在Redis集群版实例中,事务、脚本等命令要求的key必须在同一slot中,否则会返回错误信息:command keys must in same slot。
2 Redis调用Lua脚本
2.1 EVAL指令
Eval语法:
eval script numkeys key [key …] arg [arg …]
script: lua脚本
numkeys:表示有几个Key,分别是KEYS[1],KEYS[2],….,从第numkeys+1开始是参数值,ARGV[1],ARGV[2],……
注意:EVAL命令根据参数numkeys来将后面的所有参数分别存入脚本中KEYS和ARGV两个table类型的全局变量。当脚本不需要任何参数时,也不能省略这个参数,应设为0。
示例:
2.2 EVALSHA和SCRIPT LOAD指令
EVAL可以将脚本内容传递到服务端执行,可是如果脚本内容很长,而且客户端频繁执行的话,会浪费带宽。Redis提供了SCRIPT LOAD和EVALSHA指令来解决这个问题
SCRIPT LOAD 指令用于将客户端提供的 lua 脚本传递到服务器而不执行,但是会得到脚本的唯一ID(脚本的SHA1校验和),这个唯一ID 是用来唯一标识服务器缓存的这段 lua 脚本,它是由 Redis 使用 sha1 算法揉捏脚本内容而得到的一个很长的字符串。有了这个唯一 ID,后面客户端就可以通过 EVALSHA 指令反复执行这个脚本了。通过SCRIPT LOAD上传的脚本会一直存在缓存中,除非调用了清除脚本命令SCRIPT FLUSH。
SCRIPT LOAD指令:
script load luascript
其中luascript为脚本内容,方法返回脚本内容的SHA1值。
EVALSHA指令:
EVALSHAscriptsha1 numkeys key [key …] arg [arg …]
EVALSHA命令与EVAL命令一致,但是第一个参数是Lua脚本的SHA1值。
示例:
2.3 执行Lua脚本文件
在命令行编写复杂的Lua 脚本不方便,可以将脚本存储为lua文件,然后运行redis-cli –eval命令来执行脚本。
Redis-cli –h xfraud1 –p 6379 –eval scriptpath key1 key2, arg1 arg2
其中scriptpath为Lua脚本存放的路径,脚本后面传入的是参数,通过“,”分隔为两组,前面是KEYS,后面是ARGV。参数之间要用空格分隔。
示例:
3 Lua脚本中调用Redis命令
在Lua脚本中,可以通过两种方式调用Redis命令。
- redis.call
- redis.pcall
两个函数的作用类似,区别在于错误处理机制不同。在脚本运行出现错误时,Redis会保护主线程不会因为脚本的错误导致服务器崩溃,近似于在脚本的外围有一个很大的try catch语句包裹。call调用只会向上抛出异常,客户端会输出服务器返回的通用错误消息。Lua原生没有提供try catch语句,但是lua内置了pcall(f)函数,pcall的意思是protected call ,它会让f函数运行在保护模式下,f如果出现了错误,pcall调用会返回false和错误信息。
Redis.call函数调用产生错误,脚本的返回信息如下图所示。
客户端输出的是一个通用的错误信息,而不是incr调用本应该返回的WRONGTYPE类型的错误消息。Redis内部在处理redis.call遇到错误时是向上抛出异常。Pcall调用捕获到脚本异常时会向客户端回复错误信息。如果我们将上面的call改成pcall,结果如下。
注意:在Lua脚本执行的过程中遇到了错误,同Redis的事务一样,那些通过redis.call函数已经执行过的指令对服务器状态产生的影响是无法撤销的,在编写Lua脚本时一定要小心,避免没有考虑到的判断条件导致脚本没有完全执行。
4 Lua脚本中记录日志
redis.log(loglevel,message)
loglevel 如下:
- redis.LOG_DEBUG
- redis.LOG_VERBOSE
- redis.LOG_NOTICE
- redis.LOG_WARNING
message仅仅接收String类型
举例:
redis.log(redis.LOG_WARNING,"Something is wrong with this script.")
可以查看redis的conf文件,查看Redis服务日志存放位置。当脚本中输出日志的级别等于或大于conf中设置的级别时,才会输出日志。
5 Redis与Lua数据结构对应关系
Redis数据结构 | Lua数据结构 |
Integer | Number |
Bulk | String |
Multi bulk | Table |
Status | Lua 的table中有一个OK做对应 |
Error | Lua 的table中有一个err做对应 |
Nil bulk, nil multi bulk | Lua的boolean的false |
1 | 1 |
注意:
- Lua Boolean true会变成Redis中的integer 1
- Lua中的所有number类型的数据,均会变成redis中的integer,采用截取的方式。如果需要lua返回float类型,请使用string作为返回值。
- Redis中没有对nil进行转换的简单方法,如果lua的table中的元素有nil,redis无法进行转换。
6 Lua Debugger
可以使用ldb对Lua脚本进行调试。
- LDB使用的是 server-client模式,所以它是一个远程调试器.
- Redis server 作为一个调试服务器,默认调试client端是redis-cli,其他client端可以根据redis的协议进行扩展。
- 默认情况下,Redis会fork一个进程进入隔离环境,不会影响Redis正常提供服务,但调试期间,原始Redis执行命令、脚本的结果也不会体现到fork之后的隔离环境中。每一个调试进程都是一个单独的进程。这意味着在调试一个Lua脚本的同时,Redis不会阻塞,可以进行开发或者并行调试其他脚本。这也意味着调试进程中的所有更改均会回退(roll back),这保证使用同一份数据多次调试lua脚本不会存在问题。
- redis也提供了另外一种调试模式—ldb-sync-mode,即同步模式,该模式下产生的变化将会保留,并会阻塞其他请求。
调试时,加上参数—ldb即可,示例:
redis-cli –ldb -h xfraud1 -p 6379 –eval /CFCA/luaScript/amountsum.lua testamount
7 其他约定
- Redis的Lua脚本不允许生命全局变量,防止Lua脚本泄露数据。Lua脚本可以使用2个全局变量KEYS和ARGV,这两个全局变量用于接收传递的KEY和ARG。
8 脚本示例
function string.split(input, delimiter)
input = tostring(input)
delimiter = tostring(delimiter)
if (delimiter=='') then return false end
local pos,arr = 0, {}
— for each divider found
for st,sp in function() return string.find(input, delimiter, pos, true) end do
table.insert(arr, string.sub(input, pos, st – 1))
pos = sp + 1
end
table.insert(arr, string.sub(input, pos))
return arr
end
— 脚本里所有的键都应该由KEYS数组来传递,因为所有的redis命令,在执行之前都会被分析以确定命令会对哪些键进行操作
–从有序集合中获取所有元素,每个元素的格式为string-number,以“-”分割,number部分表示数值,代码实现的功能是求取所有数值的和
local tradeSet = redis.pcall('zrange',KEYS[1],0,-1)
local sum = 0;
for k,v in pairs(tradeSet) do
local tmp = v
redis.log(redis.LOG_NOTICE,"tmp value:" .. tmp)
local tmpSplitArray = string.split(tmp,'-')
local length = table.getn(tmpSplitArray)
if(length == 2) then
local tmpAmount = tmpSplitArray[2]
redis.log(redis.LOG_NOTICE,"tmp amount:" .. tmpAmount)
sum = sum + tonumber(tmpAmount)
end
end
return tostring(sum)
9 常见问题
1) 纯函数
在Lua脚本中加入redis.call("TIME"),使用的时候会报错,这是因为Redis默认情况复制Lua脚本到备机和持久化中,如果脚本是一个非纯函数,备库中执行的时候或者宕机恢复的时候可能产生不一致的情况。Redis在3.2版本中加入了redis.replicate_commands函数来解决这个问题,在脚本第一行执行这个函数,Redis会将修改数据的命令收集起来,然后用MULTI/EXEC包裹起来,这种方式称为script effects replication,这个类似于mysql中的基于行的复制模式,将非纯函数的值计算出来,用来持久化和主从复制。
2) @user_script:1: @user_script: 1: Lua script attempted to access a non local key in a cluster node
Redis集群中,会将key分配到不同的slot,然后分配到对应的机器上,当Redis执行脚本的节点与Key存放的节点不同时,会返回错误: @user_script:1: @user_script: 1: Lua script attempted to access a non local key in a cluster node。Redis集群模式要求单个Lua脚本操作的Key必须在同一个节点上,阿里云集群版官网也有说明:在Redis集群版实例中,事务、脚本等命令要求的key必须在同一个slot中,如果不在同一个slot将返回错误信息:command keys must in same slot.
3) @enable_strict_lua:8: user_script:2: Script attempted to create global variable '***'
Lua默认变量是全局的,在脚本中应使用local变量,避免在持久化、复制的时候产生各种问题。应使用 local *** 定义局部变量