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。

示例:

 

 

 

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指令:

EVALSHA scriptsha1 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

注意:

  • 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 *** 定义局部变量

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Lua脚本Redis引入的一种功能,允许开发者使用Lua语言编写脚本传到Redis中执行。使用Lua脚本有以下好处:减少网络开销,可以将多个请求通过脚本的形式一次发送,减少网络时延;原子操作,Redis会将整个脚本作为一个整体执行,中间不会被其他请求插入,因此在脚本运行过程中无需担心会出现竞态条件,无需使用事务;复用,客户端发送的脚本会永久存在Redis中,这样其他客户端可以复用这一脚本,而不需要使用代码完成相同的逻辑。 Redis引入Lua脚本的原因是为了满足特定领域对于原子性执行若干指令的需求。虽然Redis提供了丰富的指令集,但是在某些场景下,仅使用原生命令无法满足需求。用户可以通过向Redis发送Lua脚本来执行自定义动作,并获取脚本的响应数据。Redis服务器会单线程原子性地执行Lua脚本,保证在处理脚本过程中不会被其他请求打断。 如果正在运行的脚本已经执行过写操作,即使执行SCRIPT KILL命令也无法将其杀死,因为这违反了Lua脚本的原子性执行原则。在这种情况下,唯一可行的办法是使用SHUTDOWN NOSAVE命令,通过停止整个Redis进程来停止脚本的运行,并防止不完整的信息被写入数据库中。 要将Lua脚本添加到Redis服务器的脚本缓存中,并返回给定脚本的SHA1校验和,可以使用SCRIPT LOAD命令。如果给定的脚本已经在缓存中,则不会执行任何操作。<span class="em">1</span><span class="em">2</span><span class="em">3</span><span class="em">4</span>

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值