vlambda博客
学习文章列表

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

  • 为了保证多条命令组合的原子性,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代表是全局变量

local strings val = "world"

字符串

  • 下面定义一个字符串类型的数据:

local strings val = "world"

print

  • print函数可以打印出变量的值,例如下面代码将打印world

  
    
    
  
  1. local strings val = "world"


  2. printf(val)

注释

  • "--"是Lua语言的注释

-- 注释

数组

  • 在Lua中,如果要使用类似数组的功能,可以用tables类型

  • 下面代码使用定义了一个tables类型的变量myArray,但和大多数编程语言不同的是, Lua的数组下标从1开始计算:

  
    
    
  
  1. local tables myArray = {"redis", "jedis", true, 88.0}


  2. -- true

  3. print(myArray[3])

for

  • 下面代码会计算1到100的和,关键字for以end作为结束符:

  
    
    
  
  1. local int sum = 0


  2. for i = 1, 100

  3. do

  4. sum = sum + i

  5. end


  6. -- 输出结果为5050

  7. print(sum)

  •  要遍历myArray,首先需要知道tables的长度,只需要在变量前加一个# 号即可:

  
    
    
  
  1. for i = 1, #myArray

  2. do

  3. print(myArray[i])

  4. end

  • 除此之外,Lua还提供了内置函数ipairs,使用for index,value ipairs(tables)可以遍历出所有的索引下标和值:

  
    
    
  
  1. for index,value in ipairs(myArray)

  2. do

  3. print(index)

  4. print(value)

  5. end

while

  • 下面代码同样会计算1到100的和,只不过使用的是while循环,while循环同样以end作为结束符

  
    
    
  
  1. local int sum = 0

  2. local int i = 0


  3. while i <= 100

  4. do

  5. sum = sum +i

  6. i = i + 1

  7. end


  8. --输出结果为5050

  9. print(sum)

if、else

  • 要确定数组中是否包含了jedis,有则打印true,注意if以end结尾,if后 紧跟then:

  
    
    
  
  1. local tables myArray = {"redis", "jedis", true, 88.0}


  2. for i = 1, #myArray

  3. do

  4. if myArray[i] == "jedis"

  5. then

  6. print("true")

  7. break

  8. else

  9. --do nothing

  10. end

  11. end

哈希

  • 如果要使用类似哈希的功能,同样可以使用tables类型

  • 例如下面代码 定义了一个tables,每个元素包含了key和value,其中strings1..string2是将两个字符串进行连接:

  
    
    
  
  1. local tables user_1 = {age = 28, name = "tome"}

  2. --user_1 age is 28

  3. print("user_1 age is " .. user_1["age"])

  • 如果要遍历user_1,可以使用Lua的内置函数pairs:

  
    
    
  
  1. for key,value in pairs(user_1)

  2. do print(key .. value)

  3. end

函数定义

  • 在Lua中,函数以function开头,以end结尾,funcName是函数名,中间部分是函数体

  
    
    
  
  1. function funcName()

  2. ...

  3. end

  
    
    
  
  1. -- contact函数将两个字符串拼接:

  2. function contact(str1, str2)

  3. return str1 .. str2

  4. end


  5. --"hello world"

  6. print(contact("hello ", "world"))

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

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

eval

  • EVAL命令可以直接执行Lua脚本

  • 格式如下:

eval 脚本内容 key个数 key列表 参数列表
  • 例如:下面执行一个Lua脚本,内容为“hello world”,参数为0个

  • 例如:下面使用了key列表和参数列表来为Lua脚本提供更多的灵活性:

    • 此时KEYS[1]="redis",ARGV[1]="world",所以最终的返回结果是"hello redisworld"。

eval 'return "hello " .. KEYS[1] .. ARGV[1]' 1 redis world

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

  • 如果Lua脚本较长,还可以使用redis-cli --eval直接执行文件。eval命令和--eval参数本质是一样的,客户端如果想执行Lua脚本,首先在客户端编写好Lua脚本代码,然后把脚本作为字符串发送给服务端,服务端会将执行结果返回给客户端,整个过程如下图所示:

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

evalsha

  • 除了使用eval,Redis还提供了evalsha命令来执行Lua脚本

  • 格式如下:

evalsha 脚本SHA1值 key个数 key列表 参数列表
  • 执行原理如下:

    • 首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和

    •  evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本,避免每次发送Lua脚本的开销。这样客户端就不需要每次执行脚本内容,而脚本也会常驻在服务端,脚本功能得到了复用

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

演示案例:

  • 建立一个名为lua_get.lua的脚本文件,内容如下:

return "hello " .. KEYS[1] .. ARGV[1]
  • 加载脚本:在系统命令行执行script load命令(下面会介绍)可以将脚本内容加载到Redis内存中,例如下面将lua_get.lua加载到Redis中,得到SHA1 为:"7413dc2440db1fea7c0a0bde841fa68eefaf149c"

redis-cli script load "$(cat lua_get.lua)"

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

  • 执行脚本:所以在redis-cli内部使用上面的SHA1值就可以执行该脚本了

evalsha 7413dc2440db1fea7c0a0bde841fa68eefaf149c 1 redis world

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

四、Lua中的Redis API

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

call()

  • Lua可以使用redis.call函数实现对Redis的访问

  • 例如,下面代码是Lua使用redis.call调用了Redis的set和get操作:

  
    
    
  
  1. redis.call("set", "hello", "world")

  2. redis.call("get", "hello")

  • 在Redis中执行的效果如下: 

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

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个元素,如下所示

  
    
    
  
  1. RPUSH host:user:list user:1:ratio user:8:ratio user:3:ratio user:99:ratio user:72:ratio


  2. lrange host:user:list 0 -1

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

  •  user:{id}:ratio代表用户的热度,它本身又是一个字符串类型的键:

  
    
    
  
  1. set user:1:ratio 986

  2. set user:8:ratio 762

  3. set user:3:ratio 556

  4. set user:99:ratio 400

  5. set user:72:ratio 101

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

  • ②现要求将列表内所有的键对应热度做加1操作,并且保证是原子执行, 此功能可以利用Lua脚本来实现,例如下面是一个名为lrange_and_mincr.lua脚本的内容

  
    
    
  
  1. -- 将列表中所有元素取出,赋值给mylist

  2. local mylist = redis.call("lrange", KEYS[1], 0, -1)


  3. -- 定义局部变量count=0,这个count就是最后incr的总次数

  4. local count = 0


  5. -- 遍历mylist中所有元素,每次做完count自增,最后返回count

  6. for index,key in ipairs(mylist)

  7. do

  8. redis.call("incr",key)

  9. count = count + 1

  10. end

  11. return count

  • ③并执行如下操作,返回结果为5:

redis-cli --eval lrange_and_mincr.lua host:user:list

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

  • ④执行后所有用户的热度自增1:

mget user:1:ratio user:8:ratio user:3:ratio user:99:ratio user:72:ratio

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

六、Redis管理Lua脚本

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

①script load

script load script
  • 此命令用于将Lua脚本加载到Redis内存中,上面evalsha命令的演示案例中有介绍

②script exists

scripts exists sha1 [sha2 …]
  • 此命令用于判断sha1、sha2...是否已经加载到Redis内存中

  • 例如,在上面evalsha命令介绍的演示案例中,我们加载了一个SHA1值为“7413dc2440db1fea7c0a0bde841fa68eefaf149c”的Lua脚本,现在我们进行检测,结果返回1

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

③script flush

script flush
  • 此命令用于清除Redis内存已经加载的所有Lua脚本

  • 例如:例如我们清楚上面那个SHA1值为“7413dc2440db1fea7c0a0bde841fa68eefaf149c”的脚本,执行script flush后,脚本不再存在了:

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

④script kill

script kill
  • 此命令用于杀掉正在执行的Lua脚本

  • 如果Lua脚本比较耗时,甚至Lua脚本存在问题,那么此时Lua脚本的执行会阻塞Redis,直到脚本执行完毕或者外部进行干预将其结束。例如,下面的Lua脚本会无线循环,因此客户端会阻塞

Redis:18---常用功能之(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的脚本(见下图演示案例)

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

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

  • 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服务)