Redis:18---常用功能之(Lua脚本)

2021-02-03 12:48:17 浏览数 (1)

  • 为了保证多条命令组合的原子性,Redis提供了简单的事务功能以及集成Lua脚本来解决这个问题,本文介绍Lua,事务已经在前一篇文章介绍过了

一、Lua概述

  • Lua语言是在1993年由巴西一个大学研究小组发明,其设计目标是作为嵌入式程序移植到其他应用程序,它是由C语言实现的,虽然简单小巧但是功能强大
  • 所以许多应用都选用它作为脚本语言,尤其是在游戏领域,例如大名鼎鼎的暴雪公司将Lua语言引入到“魔兽世界”这款游戏中,Rovio公司将 Lua语言作为“愤怒的小鸟”这款火爆游戏的关卡升级引擎,Web服务器Nginx 将Lua语言作为扩展,增强自身功能
  • Redis将Lua作为脚本语言可帮助开发者定制自己的Redis命令,在这之前,必须修改源

二、Lua的基本语法

  • 现在先简单地介绍一下Lua的基本语法(只介绍部分语法),与Redis无关,纯属于Lua的语法

数据类型

  • Lua语言提供了如下几种数据类型:booleans(布尔)、numbers(数值)、strings(字符串)、tables(表格),和许多高级语言相比,相对简单

全局变量/局部变量

  • local代表val是一个局部变量,如果没有local代表是全局变量
代码语言:javascript复制
local strings val = "world"

字符串

  • 下面定义一个字符串类型的数据:
代码语言:javascript复制
local strings val = "world"

print

  • print函数可以打印出变量的值,例如下面代码将打印world
代码语言:javascript复制
local strings val = "world"
printf(val)

注释

  • "--"是Lua语言的注释
代码语言:javascript复制
-- 注释

数组

  • 在Lua中,如果要使用类似数组的功能,可以用tables类型
  • 下面代码使用定义了一个tables类型的变量myArray,但和大多数编程语言不同的是, Lua的数组下标从1开始计算:
代码语言:javascript复制
local tables myArray = {"redis", "jedis", true, 88.0}
-- trueprint(myArray[3])

for

  • 下面代码会计算1到100的和,关键字for以end作为结束符:
代码语言:javascript复制
local int sum = 0
for i = 1, 100dosum = sum   iend
-- 输出结果为5050print(sum)
  • 要遍历myArray,首先需要知道tables的长度,只需要在变量前加一个# 号即可:
代码语言:javascript复制
for i = 1, #myArraydoprint(myArray[i])end
  • 除此之外,Lua还提供了内置函数ipairs,使用for index,value ipairs(tables)可以遍历出所有的索引下标和值:
代码语言:javascript复制
for index,value in ipairs(myArray)doprint(index)print(value)end

while

  • 下面代码同样会计算1到100的和,只不过使用的是while循环,while循环同样以end作为结束符
代码语言:javascript复制
local int sum = 0local int i = 0
while i <= 100dosum = sum  ii = i   1end
--输出结果为5050print(sum)

if、else

  • 要确定数组中是否包含了jedis,有则打印true,注意if以end结尾,if后 紧跟then:
代码语言:javascript复制
local tables myArray = {"redis", "jedis", true, 88.0}
for i = 1, #myArraydoif myArray[i] == "jedis"thenprint("true")breakelse--do nothingendend

哈希

  • 如果要使用类似哈希的功能,同样可以使用tables类型
  • 例如下面代码 定义了一个tables,每个元素包含了key和value,其中strings1..string2是将两个字符串进行连接:
代码语言:javascript复制
local tables user_1 = {age = 28, name = "tome"}--user_1 age is 28print("user_1 age is " .. user_1["age"])
  • 如果要遍历user_1,可以使用Lua的内置函数pairs:
代码语言:javascript复制
for key,value in pairs(user_1)do print(key .. value)end

函数定义

  • 在Lua中,函数以function开头,以end结尾,funcName是函数名,中间部分是函数体
代码语言:javascript复制
function funcName()...end
代码语言:javascript复制
-- contact函数将两个字符串拼接:function contact(str1, str2)return str1 .. str2end
--"hello world"print(contact("hello ", "world"))

三、在Redis中使用Lua(eval、evalsha)

  • 在Redis中执行Lua脚本有两种方法:eval和evalsha

eval

  • EVAL命令可以直接执行Lua脚本
  • 格式如下:
代码语言:javascript复制
eval 脚本内容 key个数 key列表 参数列表
  • 例如:下面执行一个Lua脚本,内容为“hello world”,参数为0个
  • 例如:下面使用了key列表和参数列表来为Lua脚本提供更多的灵活性:
    • 此时KEYS[1]="redis",ARGV[1]="world",所以最终的返回结果是"hello redisworld"。
代码语言:javascript复制
eval 'return "hello " .. KEYS[1] .. ARGV[1]' 1 redis world
  • 如果Lua脚本较长,还可以使用redis-cli --eval直接执行文件。eval命令和--eval参数本质是一样的,客户端如果想执行Lua脚本,首先在客户端编写好Lua脚本代码,然后把脚本作为字符串发送给服务端,服务端会将执行结果返回给客户端,整个过程如下图所示:

evalsha

  • 除了使用eval,Redis还提供了evalsha命令来执行Lua脚本
  • 格式如下:
代码语言:javascript复制
evalsha 脚本SHA1值 key个数 key列表 参数列表
  • 执行原理如下:
    • 首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和
    • evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本,避免每次发送Lua脚本的开销。这样客户端就不需要每次执行脚本内容,而脚本也会常驻在服务端,脚本功能得到了复用

演示案例:

  • 建立一个名为lua_get.lua的脚本文件,内容如下:
代码语言:javascript复制
return "hello " .. KEYS[1] .. ARGV[1]
  • 加载脚本:在系统命令行执行script load命令(下面会介绍)可以将脚本内容加载到Redis内存中,例如下面将lua_get.lua加载到Redis中,得到SHA1 为:"7413dc2440db1fea7c0a0bde841fa68eefaf149c"
代码语言:javascript复制
redis-cli script load "$(cat lua_get.lua)"
  • 执行脚本:所以在redis-cli内部使用上面的SHA1值就可以执行该脚本了
代码语言:javascript复制
evalsha 7413dc2440db1fea7c0a0bde841fa68eefaf149c 1 redis world

四、Lua中的Redis API

  • 下面介绍几个在Lua中可以使用的Redis API,更多的API可以百度自行查阅

call()

  • Lua可以使用redis.call函数实现对Redis的访问
  • 例如,下面代码是Lua使用redis.call调用了Redis的set和get操作:
代码语言:javascript复制
redis.call("set", "hello", "world")redis.call("get", "hello")
  • 在Redis中执行的效果如下:

pcall()

  • 除此之外Lua还可以使用redis.pcall函数实现对Redis的调用
  • redis.call和 redis.pcall的不同在于:如果redis.call执行失败,那么脚本执行结束会直接返 回错误,而redis.pcall会忽略错误继续执行脚本,所以在实际开发中要根据 具体的应用场景进行函数的选择。

log()

  • Lua可以使用redis.log函数将Lua脚本的日志输出到Redis的日志文件中, 但是一定要控制日志级别
  • 备注:Redis3.2提供了Lua Script Debugger功能用来调试复杂的Lua脚本,具体 可以参考:http://redis.io/topics/ldb

五、使用案例

  • Lua脚本功能为Redis开发和运维人员带来如下三个好处:
    • Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令
    • Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这些命令常驻在Redis内存中,实现复用的效果
    • Lua脚本可以将多条命令一次性打包,有效地减少网络开销

演示案例

  • ①当前列表记录着热门用户的id, 假设这个列表有5个元素,如下所示
代码语言:javascript复制
RPUSH host:user:list user:1:ratio user:8:ratio user:3:ratio user:99:ratio user:72:ratio
lrange host:user:list 0 -1
  • user:{id}:ratio代表用户的热度,它本身又是一个字符串类型的键:
代码语言:javascript复制
set user:1:ratio 986set user:8:ratio 762set user:3:ratio 556set user:99:ratio 400set user:72:ratio 101
  • ②现要求将列表内所有的键对应热度做加1操作,并且保证是原子执行, 此功能可以利用Lua脚本来实现,例如下面是一个名为lrange_and_mincr.lua脚本的内容
代码语言:javascript复制
-- 将列表中所有元素取出,赋值给mylistlocal mylist = redis.call("lrange", KEYS[1], 0, -1)
-- 定义局部变量count=0,这个count就是最后incr的总次数local count = 0
-- 遍历mylist中所有元素,每次做完count自增,最后返回countfor index,key in ipairs(mylist)doredis.call("incr",key)count = count   1endreturn count
  • ③并执行如下操作,返回结果为5:
代码语言:javascript复制
redis-cli --eval lrange_and_mincr.lua host:user:list
  • ④执行后所有用户的热度自增1:
代码语言:javascript复制
mget user:1:ratio user:8:ratio user:3:ratio user:99:ratio user:72:ratio

六、Redis管理Lua脚本

  • Redis提供了4个命令实现对Lua脚本的管理,下面分别介绍

①script load

代码语言:javascript复制
script load script
  • 此命令用于将Lua脚本加载到Redis内存中,上面evalsha命令的演示案例中有介绍

②script exists

代码语言:javascript复制
scripts exists sha1 [sha2 …]
  • 此命令用于判断sha1、sha2...是否已经加载到Redis内存中
  • 例如,在上面evalsha命令介绍的演示案例中,我们加载了一个SHA1值为“7413dc2440db1fea7c0a0bde841fa68eefaf149c”的Lua脚本,现在我们进行检测,结果返回1

③script flush

代码语言:javascript复制
script flush
  • 此命令用于清除Redis内存已经加载的所有Lua脚本
  • 例如:例如我们清楚上面那个SHA1值为“7413dc2440db1fea7c0a0bde841fa68eefaf149c”的脚本,执行script flush后,脚本不再存在了:

④script kill

代码语言:javascript复制
script kill
  • 此命令用于杀掉正在执行的Lua脚本
  • 如果Lua脚本比较耗时,甚至Lua脚本存在问题,那么此时Lua脚本的执行会阻塞Redis,直到脚本执行完毕或者外部进行干预将其结束。例如,下面的Lua脚本会无线循环,因此客户端会阻塞
  • lua-time-limit参数:
    • Redis提供了一个lua-time-limit参数,单位为毫秒,默认为5000毫秒(5秒),它是Lua脚本的“超时时间”
    • 但这个超时时间仅仅是当Lua脚本时间超过lua-time-limit后,向其他命令调用发送BUSY的信号,但是并不会停止掉服务端和客户端的脚本执行,所以当达到lua-time-limit值之后,其他客户端在执行正常的命令时,将会收到“Busy Redis is busy running a script”错误,并且提示使用script kill或者shutdown nosave命令来杀掉这个busy的脚本(见下图演示案例)
  • script kill比shutdown命令更好:在Lua阻塞时,使用script kill更好,因为shutdown会让Redis服务停止,而script不会,其只是关闭Lua脚本的执行

演示案例

  • 接着上图,我们左侧的客户端执行的Lua脚本处于阻塞状态,此时我们在右边输入script kill杀死正在执行的Lua脚本,此时左侧客户端返回,所有客户端可以继续执行访问了

scritp kill命令失效的情况

  • 如果Lua脚本正在执行写操作,那么script kill命令就会失效
    • 例如,下面左侧客户端执行的Lua脚本一直不停的在执行set操作,右侧客户端使用script kill时会显示出错(上提示Lua脚本正在向Redis执行写命令,要么等待脚本执行结束要么使用shutdown nosave停掉Redis服务)

0 人点赞