Redis Lua脚本

释放双眼,带上耳机,听听看~!

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。

示例:

 Redis Lua脚本

 

 

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值。

示例:

 Redis Lua脚本

 

 

 

2.3   执行Lua脚本文件

在命令行编写复杂的Lua 脚本不方便,可以将脚本存储为lua文件,然后运行redis-cli –eval命令来执行脚本。

Redis-cli –h xfraud1 –p 6379 –eval scriptpath key1 key2, arg1 arg2

其中scriptpath为Lua脚本存放的路径,脚本后面传入的是参数,通过“,”分隔为两组,前面是KEYS,后面是ARGV。参数之间要用空格分隔。

示例:

 Redis Lua脚本

 

 

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函数调用产生错误,脚本的返回信息如下图所示。

 Redis Lua脚本

 

 

客户端输出的是一个通用的错误信息,而不是incr调用本应该返回的WRONGTYPE类型的错误消息。Redis内部在处理redis.call遇到错误时是向上抛出异常。Pcall调用捕获到脚本异常时会向客户端回复错误信息。如果我们将上面的call改成pcall,结果如下。

 Redis Lua脚本

 

 

注意:在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 *** 定义局部变量

给TA打赏
共{{data.count}}人
人已打赏
安全运维

OpenSSH-8.7p1离线升级修复安全漏洞

2021-10-23 10:13:25

安全运维

设计模式的设计原则

2021-12-12 17:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索