Redis Lua脚本开发之从入门到忘记

一、Lua快速入门

1.1 Lua简介

Lua是一门简洁小巧的脚本语言,由C语言编写,一般用来做扩展程序,嵌入在Nginx/Redis等中间件或者其它牛逼语言中使用。

1.1.1 Lua特性

  • 轻量级:它用标准C语言编写并以源代码形式开放,编译后体积很小,可以很方便的嵌入别的程序里;
  • 可扩展:Lua提供了非常易于使用的扩展接口和机制,由宿主语言提供这些功能,Lua可以使用它们,就像是本来就内置的功能一样;
  • 支持面向过程(procedure-oriented)编程和函数式编程(functional programming);
    自动内存管理;
  • 内置模式匹配和闭包;
  • 提供多线程(协同进程,并非操作系统所支持的线程)支持;
  • 通过闭包和table可以很方便地支持面向对象编程所需要的一些关键机制,比如数据抽象,虚函数,继承和重载等;

1.1.2 应用场景

  • 游戏开发
  • 独立应用脚本
  • Web 应用脚本
  • 扩展和插件,如Nginx、Redis
  • 安全系统,如入侵检测系统

1.2 环境搭建

要开发调试Lua程序,最低配的玩法就是文本编辑器+Lua解释器/编译器。

1.2.1 Windows安装

https://code.google.com/archive/p/luaforwindows

1.2.2 Linux安装

wget http://www.lua.org/ftp/lua-5.2.3.tar.gz
tar zxf lua-5.2.3.tar.gz
cd lua-5.2.3
make linux test

1.2.3 Mac安装

curl -R -O http://www.lua.org/ftp/lua-5.2.3.tar.gz
tar zxf lua-5.2.3.tar.gz
cd lua-5.2.3
make macosx test

或者使用brew安装:

brew install lua

1.3 Lua基本语法

1.3.1 注释

注释以"–"开头,如:

--[[ my first program in Lua --]]

1.3.2 标识符

标识符用于标记变量、函数或者其它用户定义的项目。合法的标识符包含字母、下划线以及数字,其中数字不能位于首字符。例如:

mohd zara abc move_name a_123
myname50 _temp j a23b9 retVal

1.3.3 关键字

Redis Lua脚本开发之从入门到忘记_第1张图片

1.3.4 变量

  1. Lua的变量包括:
  • 全局:没有使用local定义的变量就是全局变量,b
  • 局部:使用local定义的变量,local a
  • Table成员:t[1]
  1. 变量定义
local d , f = 5 ,10 --declaration of d and f as local variables. 
d , f = 5, 10; --declaration of d and f as global variables. 
d, f = 10 --[[declaration of d and f as global variables. Here value of f is nil --]]
  1. 变量声明
-- Variable definition:
local a, b
-- Initialization
a = 10
b = 30
print("value of a:", a)
print("value of b:", b)
-- Swapping of variables
b, a = a, b
print("value of a:", a)
print("value of b:", b)
f = 70.0/3.0
print("value of f", f)

1.3.5 数据类型

Redis Lua脚本开发之从入门到忘记_第2张图片
eg.

print(type("What is my type")) --> string
t = 10
print(type(5.8*t)) --> number
print(type(true)) --> boolean
print(type(print)) --> function
print(type(nil)) --> nil
print(type(type(ABC))) --> string

1.3.6 运算符和表达式

  1. 算术运算符
    Redis Lua脚本开发之从入门到忘记_第3张图片
  2. 关系运算符
    Redis Lua脚本开发之从入门到忘记_第4张图片
  3. 逻辑运算符
    Redis Lua脚本开发之从入门到忘记_第5张图片
  4. 其它
    Redis Lua脚本开发之从入门到忘记_第6张图片
  5. 三目运算符
    Lua没有提供三目运算符,但是使用逻辑运算符and和or可以实现类似效果:
value = condition and trueval or falseval;
  1. 运算符优先级
    Redis Lua脚本开发之从入门到忘记_第7张图片

1.3.7 语句

  1. 循环
  • while loop
    Redis Lua脚本开发之从入门到忘记_第8张图片
    eg.
a = 10
while( a < 20 )
do
   print("value of a:", a)
   a = a+1
end
  • for loop
    Redis Lua脚本开发之从入门到忘记_第9张图片
    eg.
for i = 10,1,-1
do
   print(i) 
end
  • repeat … until loop
    Redis Lua脚本开发之从入门到忘记_第10张图片
    eg.
--[ local variable definition --]
a = 10
--[ repeat loop execution --]
repeat
   print("value of a:", a)
   a = a + 1
until( a > 15 )
  • nested loop
    Lua的循环可以嵌套,例如:
while(condition)
do
   while(condition)
   do
      statement(s)
   end
   statement(s)
end
  • break
    可以使用break语句提前跳出循环,如下图示:
    Redis Lua脚本开发之从入门到忘记_第11张图片
    值得注意的是,Lua中没有标号语句,跳出多层嵌套循环时候有点不便。
  1. 分支
  • if
    Redis Lua脚本开发之从入门到忘记_第12张图片
    eg.
--[ local variable definition --]
a = 10;
--[ check the boolean condition using if statement --]
if( a < 20 )
then
   --[ if condition is true then print the following --]
   print("a is less than 20" );
end
print("value of a is :", a);
  • if … else
    Redis Lua脚本开发之从入门到忘记_第13张图片
    eg.
--[ local variable definition --]
a = 100;
--[ check the boolean condition --]
if( a < 20 )
then
   --[ if condition is true then print the following --]
   print("a is less than 20" )
else
   --[ if condition is false then print the following --]
   print("a is not less than 20" )
end
print("value of a is :", a)
--[ local variable definition --]
a = 100
--[ check the boolean condition --]
if( a == 10 )
then
   --[ if condition is true then print the following --]
   print("Value of a is 10" )
elseif( a == 20 )
then   
   --[ if else if condition is true --]
   print("Value of a is 20" )
elseif( a == 30 )
then
   --[ if else if condition is true  --]
   print("Value of a is 30" )
else
   --[ if none of the conditions is true --]
   print("None of the values is matching" )
end
print("Exact value of a is: ", a )
  • nested if
    eg.
--[ local variable definition --]
a = 100;
b = 200;
--[ check the boolean condition --]
if( a == 100 )
then
   --[ if condition is true then check the following --]
   if( b == 200 )
   then
      --[ if condition is true then print the following --]
      print("Value of a is 100 and b is 200" );
   end
end
print("Exact value of a is :", a );
print("Exact value of b is :", b );

1.3.8 函数

  • 函数定义
optional_function_scope function function_name( argument1, argument2, argument3........, 
argumentn)
    function_body
    return result_params_comma_separated
end
  • 函数调用
    eg.
function max(num1, num2)
   if (num1 > num2) then
      result = num1;
   else
      result = num2;
   end
   return result; 
end
-- calling a function
print("The maximum of the two numbers is ",max(10,4))
print("The maximum of the two numbers is ",max(5,6))
  • 函数作为参数
myprint = function(param)
   print("This is my print function -   ##",param,"##")
end
function add(num1,num2,functionPrint)
   result = num1 + num2
   functionPrint(result)
end
myprint(10)
add(2,5,myprint)
  • 变长参数
function average(...)
   result = 0
   local arg = {...}
   for i,v in ipairs(arg) do
      result = result + v
   end
   return result/#arg
end
print("The average is",average(10,5,3,4,5,6))
  • 返回值
    Lua的return可以返回多个值,以逗号分隔。

1.3.9 字符串

  • 字符串定义
    eg.
string1 = "Lua"
print("\"String 1 is\"",string1)
string2 = 'Tutorial'
print("String 2 is",string2)
string3 = [["Lua Tutorial"]]
print("String 3 is",string3)
  • 转义字符
    Redis Lua脚本开发之从入门到忘记_第14张图片
  • 字符串操作
    Redis Lua脚本开发之从入门到忘记_第15张图片

1.3.10 迭代

  • 通用迭代
array = {"Lua", "Tutorial"}
for key,value in ipairs(array) 
do
   print(key, value)
end
  • 无状态迭代

无状态的迭代器是指不保留任何状态的迭代器,因此在循环中我们可以利用无状态迭代器避免创建闭包花费额外的代价。
每一次迭代,迭代函数都是用两个变量(状态常量和控制变量)的值作为参数被调用,一个无状态的迭代器只利用这两个值可以获取下一个元素。
这种无状态迭代器的典型的简单的例子是ipairs,它遍历数组的每一个元素。

function square(iteratorMaxCount,currentNumber)
   if currentNumber
  • 有状态迭代
    很多情况下,迭代器需要保存多个状态信息而不是简单的状态常量和控制变量,最简单的方法是使用闭包,还有一种方法就是将所有的状态信息封装到table内,将table作为迭代器的状态常量,因为这种情况下可以将所有的信息存放在table内,所以迭代函数通常不需要第二个参数。
array = {"Lua", "Tutorial"}
function elementIterator (collection)
   local index = 0
   local count = #collection
     
   -- The closure function is returned
     
   return function ()
      index = index + 1
         
      if index <= count
      then
         -- return the current element of the iterator
         return collection[index]
      end
         
   end
     
end
for element in elementIterator(array)
do
   print(element)
end

1.3.11 Table

  • 定义和初始化
-- Simple empty table
mytable = {}
print("Type of mytable is ",type(mytable))
mytable[1]= "Lua"
mytable["wow"] = "Tutorial"
print("mytable Element at index 1 is ", mytable[1])
print("mytable Element at index wow is ", mytable["wow"])
-- alternatetable and mytable refers to same table
alternatetable = mytable
print("alternatetable Element at index 1 is ", alternatetable[1])
print("mytable Element at index wow is ", alternatetable["wow"])
alternatetable["wow"] = "I changed it"
print("mytable Element at index wow is ", mytable["wow"])
-- only variable released and and not table
alternatetable = nil
print("alternatetable is ", alternatetable)
-- mytable is still accessible
print("mytable Element at index wow is ", mytable["wow"])
mytable = nil
print("mytable is ", mytable)
  • 操作
    Redis Lua脚本开发之从入门到忘记_第16张图片

二、在Redis中的Lua

2.1 eval命令

2.1.1 指令格式

从 Redis 2.6.0 版本开始,通过内置的Lua解释器,可以使用EVAL命令对 Lua 脚本进行求值。命令格式如下:

EVAL script numkeys key [key ...] arg [arg ...]

eg.

eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second

所有的 Redis 命令,在执行之前都会被分析,籍此来确定命令会对哪些键进行操作。脚本功能被设计成与集群功能兼容,使用正确形式来传递KEY可以确保 Redis 集群可以将你的请求发送到正确的集群节点。

2.1.2 在Lua中调用Redis命令

在Lua脚本中可以使用redis.call()和redis.pcall()来执行Redis命令:

eval "return redis.call('set',KEYS[1],'bar')" 1 foo

当 redis.call() 在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,错误的输出信息会说明错误造成的原因:

redis> lpush foo a
(integer) 1
redis> eval "return redis.call('get', 'foo')" 0
(error) ERR Error running script (call to f_282297a0228f48cd3fc6a55de6316f31422f5d17): ERR Operation against a key holding the wrong kind of value

redis.pcall() 出错时并不引发(raise)错误,而是返回一个带 err 域的 Lua 表(table),用于表示错误

redis 127.0.0.1:6379> EVAL "return redis.pcall('get', 'foo')" 0
(error) ERR Operation against a key holding the wrong kind of value

2.2 Lua与Redis之间的数据类型转换

当 Lua 通过call()或pcall()函数执行 Redis 命令的时候,命令的返回值会被转换成Lua数据结构。同样地,当Lua脚本在Redis内置的解释器里运行时,Lua脚本的返回值也会被转换成Redis协议(protocol),然后由EVAL将值返回给客户端。

2.2.1 从Redis转换到Lua

  • Redis integer reply -> Lua number / Redis 整数转换成 Lua 数字
  • Redis bulk reply -> Lua string / Redis bulk 回复转换成 Lua 字符串
  • Redis multi bulk reply -> Lua table (may have other Redis data types nested) / Redis 多条 bulk 回复转换成 Lua 表,表内可能有其他别的 Redis 数据类型
  • Redis status reply -> Lua table with a single ok field containing the status / Redis 状态回复转换成 Lua 表,表内的 ok 域包含了状态信息
  • Redis error reply -> Lua table with a single err field containing the error / Redis 错误回复转换成 Lua 表,表内的 err 域包含了错误信息
  • Redis Nil bulk reply and Nil multi bulk reply -> Lua false boolean type / Redis 的 Nil 回复和 Nil 多条回复转换成 Lua 的布尔值 false

2.2.2 从Lua转换到Redis

  • Lua number -> Redis integer reply / Lua 数字转换成 Redis 整数
  • Lua string -> Redis bulk reply / Lua 字符串转换成 Redis bulk 回复
  • Lua table (array) -> Redis multi bulk reply / Lua 表(数组)转换成 Redis 多条 bulk 回复
  • Lua table with a single ok field -> Redis status reply / 一个带单个 ok 域的 Lua 表,转换成 Redis 状态回复
  • Lua table with a single err field -> Redis error reply / 一个带单个 err 域的 Lua 表,转换成 Redis 错误回复
  • Lua boolean false -> Redis Nil bulk reply / Lua 的布尔值 false 转换成 Redis 的 Nil bulk 回复
  • Lua boolean true -> Redis integer reply with value of 1 / Lua 布尔值 true 转换成 Redis 整数回复中的 1

2.3 evalsha命令

2.3.1 使用说明

为了减少带宽的消耗,Redis实现了EVALSHA命令,它的作用和EVAL一样,都用于对脚本求值,但它接受的第一个参数不是脚本,而是脚本的SHA1校验和(sum)。
如果服务器还记得给定的 SHA1 校验和所指定的脚本,那么执行这个脚本;如果服务器不记得给定的 SHA1 校验和所指定的脚本,那么它返回一个特殊的错误,提醒用户使用EVAL代替EVALSHA。

> set foo bar
OK
> eval "return redis.call('get','foo')" 0
"bar"
> evalsha 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 0
"bar"
> evalsha ffffffffffffffffffffffffffffffffffffffff 0
(error) `NOSCRIPT` No matching script. Please use [EVAL](/commands/eval).

2.3.2 脚本缓存

Redis 保证所有被运行过的脚本都会被永久保存在脚本缓存当中,这意味着,当EVAL命令在一个 Redis 实例上成功执行某个脚本之后,随后针对这个脚本的所有EVALSHA命令都会成功执行。
刷新脚本缓存的唯一办法是显式地调用 SCRIPT FLUSH 命令,这个命令会清空运行过的所有脚本的缓存。

2.3.3 在流水线中的evalsha

在流水线请求的上下文中使用EVALSHA命令时,要特别小心,因为一旦在流水线中因为EVALSHA命令而发生NOSCRIPT错误,那么这个流水线就再也没有办法重新执行了。客户端可以采用如下措施避免:

  • 总是在流水线中使用EVAL命令
  • 检查流水线中要用到的所有命令,找到其中的 EVAL 命令,并使用 SCRIPT EXISTS 命令检查要用到的脚本是不是全都已经保存在缓存里面了。如果所需的全部脚本都可以在缓存里找到,那么就可以放心地将所有 EVAL 命令改成 EVALSHA 命令,否则的话,就要在流水线的顶端(top)将缺少的脚本用 SCRIPT LOAD 命令加上去。

2.4 script命令

Redis 提供了以下几个SCRIPT命令,用于对脚本子系统(scripting subsystem)进行控制:

  • SCRIPT FLUSH :清除所有脚本缓存
  • SCRIPT EXISTS :根据给定的脚本校验和,检查指定的脚本是否存在于脚本缓存
  • SCRIPT LOAD :将一个脚本装入脚本缓存,但并不立即运行它
  • SCRIPT KILL :杀死当前正在运行的脚本

2.5 Redis对Lua的一些限制

2.5.1 纯函数脚本

  • 对于同样的数据集输入,给定相同的参数,脚本执行的 Redis 写命令总是相同的;
  • Lua 没有访问系统时间或者其他内部状态的命令;
  • 每当从 Lua 脚本中调用那些返回无序元素的命令时,执行命令所得的数据在返回给 Lua 之前会先执行一个静默(slient)的字典序排序(lexicographical sorting);
  • 对 Lua 的伪随机数生成函数 math.random 和 math.randomseed 进行修改,使得每次在运行新脚本的时候,总是拥有同样的 seed 值。这意味着,每次运行脚本时,只要不使用 math.randomseed ,那么 math.random 产生的随机数序列总是相同的;

2.5.2 全局变量保护

为了防止不必要的数据泄漏进 Lua 环境, Redis 脚本不允许创建全局变量。如果一个脚本需要在多次执行之间维持某种状态,它应该使用Redis key来进行状态保存。企图在脚本中访问一个全局变量(不论这个变量是否存在)将引起脚本停止,EVAL命令会返回一个错误:

redis 127.0.0.1:6379> eval 'a=10' 0
(error) ERR Error running script (call to f_933044db579a2f8fd45d8065f04a8d0249383e57): user_script:1: Script attempted to create global variable 'a'

2.5.3 内置库

Redis内置的Lua解释器加载了以下Lua库:

  • base
  • table
  • string
  • math
  • debug
  • cjson
  • cmsgpack

2.5.4 沙箱

脚本应该仅仅用于传递参数和对Redis数据进行处理,它不应该尝试去访问外部系统(比如文件系统),或者执行任何系统调用。

2.5.5 最大执行时间

脚本有一个最大执行时间限制,它的默认值是5秒钟,由lua-time-limit选项来控制(以毫秒为单位),可以通过编辑redis.conf文件或者使用CONFIG GET和CONFIG SET命令来修改它。
当脚本运行的时间超过最大执行时间后,以下动作会被执行:

  • Redis 记录一个脚本正在超时运行
  • Redis 开始重新接受其他客户端的命令请求,但是只有 SCRIPT KILL 和 SHUTDOWN NOSAVE 两个命令会被处理,对于其他命令请求, Redis 服务器只是简单地返回 BUSY 错误;
  • 可以使用 SCRIPT KILL 命令将一个仅执行只读命令的脚本杀死,因为只读命令并不修改数据,因此杀死这个脚本并不破坏数据的完整性;
  • 如果脚本已经执行过写命令,那么唯一允许执行的操作就是 SHUTDOWN NOSAVE ,它通过停止服务器来阻止当前数据集写入磁盘;

三、开发调试

3.1 使用ldb调试

Redis从3.2开始提供了一个完整的Lua调试器,代号为ldb。

3.1.1 特性

  • ldb是一个基于C/S模式开发的远程调试器;
  • 每次debug session是一个forked session,调试过程中不会阻塞Redis接收其它客户端的命令,调试完毕后Redis会回滚调试会话中所做的任何修改;
  • 可以使用同步模式进行调试,这样会阻塞Redis接收其它客户端的命令,调试过程中所做的修改也不会回滚;
  • 支持单步调试;
  • 支持静态和动态断点;
  • 支持打印调试日志到命令行;
  • 可以查看和打印Lua变量取值;

3.1.2 使用说明

  1. 命令格式
./redis-cli --ldb --eval /tmp/script.lua mykey somekey , arg1 arg2

逗号前面为key列表,逗号后面为参数列表,注意逗号前后都需要有空格,否则会报错。
进入到调试模式后,Redis只支持如下三个命令:

  • quit:退出调试会话,同时退出redis-cli;
  • restart:重新加载脚本,重新进入调试会话;
  • help:打印如下帮助信息:
lua debugger> help
Redis Lua debugger help:
[h]elp               Show this help.
[s]tep               Run current line and stop again.
[n]ext               Alias for step.
[c]continue          Run till next breakpoint.
[l]list              List source code around current line.
[l]list [line]       List source code around [line].
                     line = 0 means: current position.
[l]list [line] [ctx] In this form [ctx] specifies how many lines
                     to show before/after [line].
[w]hole              List all source code. Alias for 'list 1 1000000'.
[p]rint              Show all the local variables.
[p]rint <var>        Show the value of the specified variable.
                     Can also show global vars KEYS and ARGV.
[b]reak              Show all breakpoints.
[b]reak <line>       Add a breakpoint to the specified line.
[b]reak -<line>      Remove breakpoint from the specified line.
[b]reak 0            Remove all breakpoints.
[t]race              Show a backtrace.
[e]eval <code>       Execute some Lua code (in a different callframe).
[r]edis <cmd>        Execute a Redis command.
[m]axlen [len]       Trim logged Redis replies and Lua var dumps to len.
                     Specifying zero as <len> means unlimited.
[a]abort             Stop the execution of the script. In sync
                     mode dataset changes will be retained.
Debugger functions you can call from Lua scripts:
redis.debug()        Produce logs in the debugger console.
redis.breakpoint()   Stop execution as if there was a breakpoint in the
                     next line of code.
  1. 断点
    在控制台输入b 3,就可以在脚本的第三行打上断点。
    在Lua脚本中还可以使用动态断点:
if counter > 10 then redis.breakpoint() end

在调试中,输入s可以单步执行,使用c可以执行到下一个断点位置。
3. 同步模式
加上–ldb-sync-mode即可进入同步调试模式。
4. 打印变量
在调试命令行中输入p var可以在控制台中打印出变量var的取值。
5. 执行命令
在调试中,还可以输入e执行一些Lua脚本,不过会在另外一个调用帧中执行,因为调试会话是一个forked session。

lua debugger> e redis.sha1hex('foo')
<retval> "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"

3.2 使用ZeroBrane Studio调试

ZeroBrane是一个免费、开源、跨平台的Lua IDE,可以很方便地开发调试Lua脚本。

3.2.1 安装ZeroBrane

打开如下页面下载对应平台的源文件并安装:
https://studio.zerobrane.com/download?not-this-time

3.2.2 安装Redis插件

下载插件
https://github.com/pkulchenko/ZeroBranePackage/blob/master/redis.lua
然后放置到ZeroBrane安装目录的packages(/opt/zbstudio/packages/)下,或者是~/.zbstudio/packages文件夹中。

3.2.3 使用ZeroBrane调试Redis Lua脚本

打开IDE,新建一个Lua脚本,然后Project -> Lua Interpreter,选择Redis
Redis Lua脚本开发之从入门到忘记_第17张图片
然后Project -> Command Line Parameters…,输入对应的key和参数,注意逗号前后都要有空格(这里实际上执行的就是Redis的eval命令)。
Redis Lua脚本开发之从入门到忘记_第18张图片
Redis Lua脚本开发之从入门到忘记_第19张图片
然后点击运行,ZeroBrane会提示输入redis地址
zerobrane enter redis addr
可以设置断点,查看变量和单步执行
Redis Lua脚本开发之从入门到忘记_第20张图片
ZeroBrane还提供了Remote Console用于远程执行Lua脚本和Redis命令(全大写或者用@开头的小写Redis命令):
Redis Lua脚本开发之从入门到忘记_第21张图片

四、参考

  • Lua - Quick Guide
  • Operators Tutorial
  • Expressions Tutorial
  • EVAL
  • Redis Lua scripts debugger
  • Quick Start Guide for Lua
  • ZeroBrane Studio Plugin for Redis Lua Scripts
  • Develop and debug Redis Lua scripts with ZeroBrane Studio

你可能感兴趣的:(Lua)