Lua

Lua语法

Lua 数据类型

Lua 是动态类型语言,变量不要类型定义,只需要为变量赋值。 值可以存储在变量中,作为参数传递或结果返回。

Lua 中有 8 个基本类型分别为:nil、boolean、number、string、userdata、function、thread 和 table。

数据类型描述
nil这个最简单,只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)。
boolean包含两个值:false和true。
number表示双精度类型的实浮点数
string字符串由一对双引号或单引号来表示。可以用 2 个方括号 [[ ]]"来表示"一块"字符串。字符串连接使用的是 ..
function由 C 或 Lua 编写的函数
userdata表示任意存储在变量中的C数据结构
thread表示执行的独立线路,用于执行协同程序
tableLua 中的表(table)其实是一个"关联数组"(associative arrays),数组的索引可以是数字、字符串或表类型。在 Lua 里,table 的创建是通过"构造表达式"来完成,最简单构造表达式是{},用来创建一个空表。

通过type()方法可以获取到变量的类型,返回值是string类型的。

type(X)
print("a" .. 'b')   --字符串连接

Lua字符串

字符串或串(String)是由数字、字母、下划线组成的一串字符。

Lua 语言中字符串可以使用以下三种方式来表示:

  • 单引号间的一串字符。

  • 双引号间的一串字符。

  • [[]] 间的一串字符。

字符串转义

与其他语言一样,使用\

字符串函数
-- 字符串全部转为大写字母。
string.upper(argument)
-- 字符串全部转为小写字母。
string.lower(argument)
--在字符串中替换。。
----mainString 为要操作的字符串, findString 为被替换的字符,replaceString 要替换的字符,num 替换次数(可以忽略,则全部替换)
string.gsub(mainString,findString,replaceString,num)
--在一个指定的目标字符串中搜索指定的内容(第三个参数为索引),返回其具体位置。不存在则返回 nil。
string.find (str, substr, [init, [end]])
--字符串反转
string.reverse(arg)
--返回一个类似printf的格式化字符串
string.format(...)
--char 将整型数字转成字符并连接, byte 转换字符为整数值(可以指定某个字符,默认第一个字符)。
string.char(...) 和 string.byte(arg[,int])
--计算字符串长度。
string.len(arg)
--返回字符串string的n个拷贝(重复n次)
string.rep(string, n)
--链接两个字符串
..
--返回一个迭代器函数,每一次调用这个函数,返回一个在字符串 str 找到的下一个符合 pattern 描述的子串。如果参数 pattern 描述的字符串没有找到,迭代函数返回nil。
string.gmatch(str, pattern)
--只寻找源字串str中的第一个配对. 参数init可选, 指定搜寻过程的起点, 默认为1。
--在成功配对时, 函数将返回配对表达式中的所有捕获结果; 如果没有设置捕获标记, 则返回整个配对字符串. 当没有成功的配对时, 返回nil。
string.match(str, pattern, init)
--截取字符串。j:截取结束位置,默认为 -1,最后一个字符。
string.sub(s, i [, j])

Lua 变量

变量在使用前,需要在代码中进行声明,即创建该变量。

Lua 变量有三种类型:全局变量局部变量表中的域

Lua 中的变量全是全局变量,那怕是语句块或是函数里,除非用 local 显式声明为局部变量。(与JavaScript类似)

局部变量的作用域为从声明位置开始到所在语句块结束。

变量的默认值均为 nil

在Lua 语言中,全局变量无须声明即可使用,使用未经初始化的全局变量不会导致错误。当使用未经初始化的全局变量时,得到的结果为 nil

当把nil赋值给全局变量时,Lua会回收该全局变量。

赋值语句
a = "hello" .. "world"
-- Lua 可以对多个变量同时赋值,变量列表和值列表的各个元素用逗号分开,赋值语句右边的值会依次赋给左边的变量。
a, b = 10, 2*x       -->       a=10; b=2*x
-- 遇到赋值语句Lua会先计算右边所有的值然后再执行赋值操作,所以我们可以这样进行交换变量的值:
x, y = y, x                     -- swap 'x' for 'y'
a[i], a[j] = a[j], a[i]         -- swap 'a[i]' for 'a[j]'

当变量个数和值的个数不一致时,Lua会一直以变量个数为基础采取以下策略:

  • a. 变量个数 > 值的个数 按变量个数补足nil

  • b. 变量个数 < 值的个数 多余的值会被忽略

*索引

对 table 的索引使用方括号 []。Lua 也提供了 . 操作。

t[i]
t.i                 -- 当索引为字符串类型时的一种简化写法
gettable_event(t,i) -- 采用索引访问本质上是一个类似这样的函数调用

语句控制

循环语句
-- 
while(condition)
do
   statements
end
-- 数值循环:var 从 exp1 变化到 exp2,每次变化以 exp3 为步长递增 var,并执行一次 "执行体"。exp3 是可选的,如果不指定,默认为1。
for var=exp1,exp2,exp3 do  
    <执行体>  
end  
--泛型for 循环。i是数组索引值,v是对应索引的数组元素值。ipairs是Lua提供的一个迭代器函数,用来迭代数组。
a = {"one", "two", "three"}
for i, v in ipairs(a) do
    print(i, v)
end 

-- 类似其他语言 do ...  while。
repeat
   statements
until( condition )

--嵌套循环。前面几个循环的互相嵌套
while(condition)
do
   while(condition)
   do
      statements
   end
   statements
end
if
if(布尔表达式)
then
   --[ 在布尔表达式为 true 时执行的语句 --]
end
--------------------------
if(布尔表达式)
then
   --[ 布尔表达式为 true 时执行该语句块 --]
else
   --[ 布尔表达式为 false 时执行该语句块 --]
end
------------------------
if( 布尔表达式 1)
then
   --[ 布尔表达式 1 为 true 时执行该语句块 --]
   if(布尔表达式 2)
   then
      --[ 布尔表达式 2 为 true 时执行该语句块 --]
   end
end

Lua认为false和nil为假,true 和非nil为真。要注意的是Lua中 0 为 true。

函数

通过关键字 function 声明一个函数。return 语句返回结果。

optional_function_scope function function_name( argument1, argument2, argument3..., argumentn)
    function_body
    return result_params_comma_separated
end

说明:

  • optional_function_scope: 该参数是可选的制定函数是全局函数还是局部函数,未设置该参数默认为全局函数,如果你需要设置函数为局部函数需要使用关键字 local
  • function_name: 指定函数名称。
  • argument1, argument2, argument3..., argumentn: 函数参数,多个参数以逗号隔开,函数也可以不带参数。
  • function_body: 函数体,函数中需要执行的代码语句块。
  • result_params_comma_separated: 函数返回值,Lua语言函数可以返回多个值,每个值以逗号隔开
*多值返回
function maximum (a)
    local mi = 1             -- 最大值索引
    local m = a[mi]          -- 最大值
    for i,val in ipairs(a) do
       if val > m then
           mi = i
           m = val
       end
    end
    return m, mi
end

print(maximum({8,10,23,12,5}))
*可变参数

Lua 函数可以接受可变数目的参数,和 C 语言类似,在函数参数列表中使用三点 表示函数有可变的参数。

function add(...)  
local s = 0  
  for i, v in ipairs{...} do   --> {...} 表示一个由所有变长参数构成的数组  。inpairs没有使用括号,应该是语法编译。
    s = s + v  
  end  
  return s  
end  
print(add(3,4,5,6,7))  --->25
  • 通常在遍历变长参数的时候只需要使用 {…},然而变长参数可能会包含一些 nil,那么就可以用 select 函数来访问变长参数了:select('#', …) 或者 select(n, …)

    • select('#', …) 返回可变参数的长度。
      • select(n, …) 用于返回从起点 n 开始到结束位置的所有参数列表。
  • 调用 select 时,必须传入一个固定实参 selector(选择开关) 和一系列变长参数。如果 selector 为数字 n,那么 select 返回参数列表中从索引 n 开始到结束位置的所有参数列表,否则只能为字符串 #,这样 select 返回变长参数的总数。

Lua运算符

算术运算符

设定 A 的值为10,B 的值为 20:

操作符描述实例
+加法A + B 输出结果 30
-减法A - B 输出结果 -10
*乘法A * B 输出结果 200
/除法B / A 输出结果 2
%取余B % A 输出结果 0
^乘幂A^2 输出结果 100
-负号-A 输出结果 -10
关系运算符

设定 A 的值为10,B 的值为 20:

操作符描述实例
==等于,检测两个值是否相等,相等返回 true,否则返回 false(A == B) 为 false。
~=不等于,检测两个值是否相等,不相等返回 true,否则返回 false(A ~= B) 为 true。
>大于,如果左边的值大于右边的值,返回 true,否则返回 false(A > B) 为 false。
<小于,如果左边的值大于右边的值,返回 false,否则返回 true(A < B) 为 true。
>=大于等于,如果左边的值大于等于右边的值,返回 true,否则返回 false(A >= B) 返回 false。
<=小于等于, 如果左边的值小于等于右边的值,返回 true,否则返回 false(A <= B) 返回 true。
逻辑运算符

设定 A 的值为 true,B 的值为 false:

操作符描述实例
and逻辑与操作符。 若 A 为 false,则返回 A,否则返回 B。(A and B) 为 false。
or逻辑或操作符。 若 A 为 true,则返回 A,否则返回 B。(A or B) 为 true。
not逻辑非操作符。与逻辑运算结果相反,如果条件为 true,逻辑非为 false。not(A and B) 为 true。
*其他运算符
操作符描述实例
..连接两个字符串a…b ,其中 a 为 "Hello " , b 为 “World”, 输出结果为 “Hello World”。
#一元运算符,返回字符串或表的长度。#“Hello” 返回 5
运算符优先级

从高到低的顺序:

^
not    - (unary)
*      /       %
+      -
..                                                ####################
<      >      <=     >=     ~=     ==
and
or

*除了 ^ 和 … 外所有的二元运算符都是左结合的。

a+i < b/2+1          -->       (a+i) < ((b/2)+1)
5+x^2*8              -->       5+((x^2)*8)
a < y and y <= z     -->       (a < y) and (y <= z)
-x^2                 -->       -(x^2)
x^y^z                -->       x^(y^z)          ::::右结合的:::,注意顺序

Lua数组

Lua 数组的索引键值可以使用整数表示,数组的大小不是固定的。

* 一维数组
array = {"Lua", "Tutorial"}

在 Lua 索引值是以 1 为起始,但你也可以指定 0 开始。

还可以以负数为数组索引值。数组的索引,不代表第几个元素,而是类似map的key

array = {}

for i= -2, 2 do
   array[i] = i *2
end

for i = -2,2 do
   print(array[i])
end
--OUTPUT:
-4
-2
0
2
4
多维数组

就是类似于value是一个map的map。

-- 初始化数组
array = {}
for i=1,3 do
   array[i] = {}
      for j=1,3 do
         array[i][j] = i*j
      end
end

Lua迭代器

泛型 for 迭代器

泛型 for 在自己内部保存迭代函数,实际上它保存三个值:迭代函数、状态常量、控制变量。

for k, v in pairs(t) do
    print(k, v)
end

泛型 for 的执行过程:

  • 首先,初始化,计算 in 后面表达式的值,表达式应该返回泛型 for 需要的三个值:迭代函数状态常量控制变量;与多值赋值一样,如果表达式返回的结果个数不足三个会自动用 nil 补足,多出部分会被忽略。
  • 第二,将状态常量和控制变量作为参数调用迭代函数(注意:对于 for 结构来说,状态常量没有用处,仅仅在初始化时获取他的值并传递给迭代函数)。
  • 第三,将迭代函数返回的值赋给变量列表
  • 第四,如果返回的第一个值为nil循环结束,否则执行循环体。
  • 第五,回到第二步再次调用迭代函数

在Lua中我们常常使用函数来描述迭代器,每次调用该函数就返回集合的下一个元素。Lua 的迭代器包含以下两种类型:

  • 无状态的迭代器
  • 多状态的迭代器
无状态的迭代器

无状态的迭代器是指不保留任何状态的迭代器,因此在循环中我们可以利用无状态迭代器避免创建闭包花费额外的代价。

每一次迭代,迭代函数都是用两个变量(状态常量和控制变量)的值作为参数被调用,一个无状态的迭代器只利用这两个值可以获取下一个元素。

这种无状态迭代器的典型的简单的例子是 ipairs,它遍历数组的每一个元素,元素的索引需要是数值。

--迭代函数
function square(iteratorMaxCount,currentNumber)
   if currentNumber<iteratorMaxCount
   then
      currentNumber = currentNumber+1
   return currentNumber, currentNumber*currentNumber
   end
end

for i,n in square,3,0
do
   print(i,n)
end

--OUTPUT:
1    1
2    4
3    9

迭代的状态包括被遍历的表(循环过程中不会改变的状态常量)和当前的索引下标(控制变量),ipairs 和迭代函数都很简单,我们在 Lua 中可以这样实现:

--迭代函数。参数a:是状态常量,i:控制变量
function iter (a, i)
    i = i + 1
    local v = a[i]
    if v then
       return i, v
    end
end
 
function ipairs (a)
    return iter, a, 0
end

当 Lua 调用 ipairs(a) 开始循环时,他获取三个值:迭代函数 iter状态常量 a控制变量初始值 0;然后 Lua 调用 iter(a,0) 返回 1, a[1](除非 a[1]=nil);第二次迭代调用 iter(a,1) 返回 2, a[2]……直到第一个 nil 元素。

多状态的迭代器

迭代器需要保存多个状态信息而不是简单的状态常量和控制变量,最简单的方法是使用闭包,还有一种方法就是将所有的状态信息封装到 table 内,将 table 作为迭代器的状态常量,因为这种情况下可以将所有的信息存放在 table 内,所以迭代函数通常不需要第二个参数。

array = {"Google", "Runoob"}

function elementIterator (collection)
   local index = 0
   local count = #collection   --collectioin 长度
   -- 闭包函数
   return function ()
      index = index + 1
      if index <= count
      then
         --  返回迭代器的当前元素
         return collection[index]
      end
   end
end

for element in elementIterator(array)  -- elementIterator(array) 调用的返回值(闭包) ,作为一个迭代函数。
do
   print(element)
end
--OUTPUT:
Google
Runoob

Lua table(表)

table 是 Lua 的一种数据结构用来帮助我们创建不同的数据类型,如:数组、字典等。

Lua table 使用关联型数组,你可以用任意类型的值来作数组的索引,但这个值不能是 nil

Lua table 是不固定大小的,你可以根据自己需要进行扩容。

Lua也是通过table来解决模块(module)、包(package)和对象(Object)的。 例如string.format表示使用"format"来索引table string。

table(表)的构造
-- 初始化表
mytable = {}

-- 指定值
mytable[1]= "Lua"

-- 移除引用
mytable = nil
-- lua 垃圾回收会释放内存
Table 操作
序号方法用途
1table.concat (table [, sep [, start [, end]]]):concat是concatenate(连锁, 连接)的缩写. table.concat()函数列出参数中指定table的数组部分从start位置到end位置的所有元素, 元素间以指定的分隔符(sep)隔开。
2table.insert (table, [pos,] value):在table的数组部分指定位置(pos)插入值为value的一个元素. pos参数可选, 默认为数组部分末尾.
3table.maxn (table)指定table中所有正数key值中最大的key值. 如果不存在key值为正数的元素, 则返回0。(Lua5.2之后该方法已经不存在了,本文使用了自定义函数实现)
4table.remove (table [, pos])返回table数组部分位于pos位置的元素. 其后的元素会被前移. pos参数可选, 默认为table长度, 即从最后一个元素删起。
5table.sort (table [, comp])对给定的table进行升序排序。

Lua其他

其他 Redis用不不涉及,就不整理了。

参考

https://www.runoob.com/lua/lua-strings.html

Redis

Redis中使用Lua的好处

  • 减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延
  • 原子操作。redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。
  • 复用。客户端发送的脚步会永久存在redis中,这样,其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑。

Redis Lua脚本与事务

从定义上来说, Redis 中的脚本本身就是一种事务, 所以任何在事务里可以完成的事, 在脚本里面也能完成。 并且一般来说, 使用脚本要来得更简单,并且速度更快。

使用事务时可能会遇上以下两种错误:

  • 事务在执行 EXEC 之前,入队的命令可能会出错。比如说,命令可能会产生语法错误(参数数量错误,参数名错误,等等),或者其他更严重的错误,比如内存不足(如果服务器使用 maxmemory 设置了最大内存限制的话)。
  • 命令可能在 EXEC 调用之后失败。举个例子,事务中的命令可能处理了错误类型的键,比如将列表命令用在了字符串键上面,诸如此类。

对于发生在 EXEC 执行之前的错误,客户端以前的做法是检查命令入队所得的返回值:如果命令入队时返回 QUEUED ,那么入队成功;否则,就是入队失败。如果有命令在入队时失败,那么大部分客户端都会停止并取消这个事务。

从 Redis 2.6.5 开始,服务器会对命令入队失败的情况进行记录,并在客户端调用 EXEC 命令时,拒绝执行并自动放弃这个事务。

经过测试lua中发生异常处理方式和redis 事务一致,可以说这两个东西是一样的,但是lua支持缓存,可以复用脚本,这个是原来的事务所没有的

Redis 相关命令

EVAL

EVAL script numkeys key [key …] arg [arg …]

script 参数是一段 Lua 5.1 脚本程序,它会被运行在 Redis 服务器上下文中,这段脚本不必(也不应该)定义为一个 Lua 函数。

numkeys 参数用于指定键名参数的个数。

键名参数 key [key ...]EVAL第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 1 为基址的形式访问( KEYS[1]KEYS[2] ,以此类推)。

在命令的最后,那些不是键名参数的附加参数 arg [arg ...] ,可以在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1]ARGV[2] ,诸如此类)。

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

EVALSHA

EVAL 命令要求在每次执行脚本的时候都发送一次脚本主体(script body)。Redis 有一个内部的缓存机制,因此它不会每次都重新编译脚本,不过在很多场合,付出无谓的带宽来传送脚本主体并不是最佳选择。

为了减少带宽的消耗, Redis 实现了 EVALSHA 命令,它的作用和 EVAL 一样,都用于对脚本求值,但它接受的第一个参数不是脚本,而是脚本的 SHA1 校验和(sum)。

EVALSHA 命令的表现如下:

  • 如果服务器还缓存了给定的 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).

客户端库的底层实现可以一直乐观地使用 EVALSHA 来代替 EVAL ,并期望着要使用的脚本已经保存在服务器上了,只有当 NOSCRIPT 错误发生时,才使用 EVAL 命令重新发送脚本,这样就可以最大限度地节省带宽。

这也说明了执行 EVAL 命令时,使用正确的格式来传递键名参数和附加参数的重要性:因为如果将参数硬写在脚本中,那么每次当参数改变的时候,都要重新发送脚本,即使脚本的主体并没有改变,相反,通过使用正确的格式来传递键名参数和附加参数,就可以在脚本主体不变的情况下,直接使用 EVALSHA 命令对脚本进行复用,免去了无谓的带宽消耗。

SCRIPT FLUSH

清除Redis服务端所有 Lua 脚本缓存

script flush

SCRIPT EXISTS

给定一个或多个脚本的 SHA1 校验和,返回一个包含 0或 1 的列表,表示校验和所指定的脚本是否已经被保存在缓存当中

SCRIPT EXISTS sha1 [sha1 …] 

SCRIPT LOAD

将脚本 script 添加到Redis服务器的脚本缓存中,并不立即执行这个脚本,而是会立即对输入的脚本进行求值。并返回给定脚本的 SHA1 校验和。如果给定的脚本已经在缓存里面了,那么不执行任何操作。

SCRIPT LOAD script 

SCRIPT KILL

杀死当前正在运行的脚本

SCRIPT KILL

杀死当前正在运行的 Lua 脚本,当且仅当这个脚本没有执行过任何写操作时,这个命令才生效。 这个命令主要用于终止运行时间过长的脚本,比如一个因为 BUG 而发生无限 loop 的脚本,诸如此类。

假如当前正在运行的脚本已经执行过写操作,那么即使执行SCRIPT KILL,也无法将它杀死,因为这是违反 Lua 脚本的原子性执行原则的。在这种情况下,唯一可行的办法是使用SHUTDOWN NOSAVE命令,通过停止整个 Redis 进程来停止脚本的运行,并防止不完整(half-written)的信息被写入数据库中。

Lua中执行redis命令

在 Lua 脚本中,可以使用两个不同函数来执行 Redis 命令,它们分别是:

  • redis.call()
  • redis.pcall()

这两个函数的唯一区别在于它们使用不同的方式处理执行命令所产生的错误

redis.call()redis.pcall() 两个函数的参数可以是任何格式良好(well formed)的 Redis 命令:

> eval "return redis.call('set','foo','bar')" 0
OK

上面这段脚本的确实现了将键 foo 的值设为 bar 的目的,但是,它违反了 EVAL 命令的语义,因为脚本里使用的所有键都应该由 KEYS 数组来传递,就像这样:

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

要求使用正确的形式来传递键(key)是有原因的,因为不仅仅是 EVAL 这个命令,所有的 Redis 命令,在执行之前都会被分析,籍此来确定命令会对哪些键进行操作。

因此,对于 EVAL 命令来说,必须使用正确的形式来传递键,才能确保分析工作正确地执行。除此之外,使用正确的形式来传递键还有很多其他好处,它的一个特别重要的用途就是确保 Redis 集群可以将你的请求发送到正确的集群节点。(对 Redis 集群的工作还在进行当中,但是脚本功能被设计成可以与集群功能保持兼容。)不过,这条规矩并不是强制性的,从而使得用户有机会滥用(abuse) Redis 单实例配置(single instance configuration),代价是这样写出的脚本不能被 Redis 集群所兼容。

在 Lua 数据类型和 Redis 数据类型之间转换

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

数据类型之间的转换遵循这样一个设计原则:如果将一个 Redis 值转换成 Lua 值,之后再将转换所得的 Lua 值转换回 Redis 值,那么这个转换所得的 Redis 值应该和最初时的 Redis 值一样。

换句话说, Lua 类型和 Redis 类型之间存在着一一对应的转换关系

以下列出的是详细的转换规则:

从 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`

从 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 转换到 Redis 有一条额外的规则,这条规则没有和它对应的从 Redis 转换到 Lua 的规则:

Lua boolean true -> Redis integer reply with value of 1  # Lua 布尔值 `true` 转换成 Redis 整数回复中的 `1`

示例:

#将 Lua 值转换成 Redis 值
> eval "return 10" 0
(integer) 10

> eval "return {1,2,{3,'Hello World!'}}" 0
1) (integer) 1
2) (integer) 2
3) 1) (integer) 3
   2) "Hello World!"
#将 Redis 值转换成 Lua 值,然后再将 Lua 值转换成 Redis 值的类型转过程。
> eval "return redis.call('get','foo')" 0
"bar"

错误处理

redis.call()redis.pcall() 的唯一区别在于它们对错误处理的不同。

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.call() 不同, 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

纯函数脚本

在编写脚本方面,一个重要的要求就是,脚本应该被写成纯函数(pure function)。

也就是说,脚本应该具有以下属性:

  • 对于同样的数据集输入,给定相同的参数,脚本执行的 Redis 写命令总是相同的。脚本执行的操作不能依赖于任何隐藏(非显式)数据,不能依赖于脚本在执行过程中、或脚本在不同执行时期之间可能变更的状态,并且它也不能依赖于任何来自 I/O 设备的外部输入。

类似于幂等操作

为了确保脚本符合上面所说的属性, Redis 做了以下工作:

  • Lua 没有访问系统时间或者其他内部状态的命令
  • Redis 会返回一个错误,阻止这样的脚本运行: 这些脚本在执行随机命令之后(比如 RANDOMKEY 、 [SRANDMEMBER key count]TIME 等),还会执行可以修改数据集的 Redis 命令。如果脚本只是执行只读操作,那么就没有这一限制。注意,随机命令并不一定就指那些带 RAND 字眼的命令,任何带有非确定性的命令都会被认为是随机命令,比如 TIME 命令就是这方面的一个很好的例子。
  • 每当从 Lua 脚本中调用那些返回无序元素的命令时,执行命令所得的数据在返回给 Lua 之前会先执行一个静默(slient)的字典序排序(lexicographical sorting)。举个例子,因为 Redis 的 Set 保存的是无序的元素,所以在 Redis 命令行客户端中直接执行 SMEMBERS key ,返回的元素是无序的,但是,假如在脚本中执行 redis.call("smembers", KEYS[1]) ,那么返回的总是排过序的元素。
  • 对 Lua 的伪随机数生成函数 math.randommath.randomseed 进行修改,使得每次在运行新脚本的时候,总是拥有同样的 seed 值。这意味着,每次运行脚本时,只要不使用 math.randomseed ,那么 math.random 产生的随机数序列总是相同的。

尽管有那么多的限制,但用户还是可以用一个简单的技巧写出带随机行为的脚本(如果他们需要的话)。

假设现在我们要编写一个 Redis 脚本,这个脚本从列表中弹出 N 个随机数。一个 Ruby 写的例子如下:

require 'rubygems'
require 'redis'

r = Redis.new

RandomPushScript = <<EOF
    local i = tonumber(ARGV[1])
    local res
    while (i > 0) do
        res = redis.call('lpush',KEYS[1],math.random())
        i = i-1
    end
    return res
EOF

r.del(:mylist)
puts r.eval(RandomPushScript,[:mylist],[10,rand(2**32)])

这个程序每次运行都会生成带有以下元素的列表:

> lrange mylist 0 -1
1) "0.74509509873814"
2) "0.87390407681181"
3) "0.36876626981831"
4) "0.6921941534114"
5) "0.7857992587545"
6) "0.57730350670279"
7) "0.87046522734243"
8) "0.09637165539729"
9) "0.74990198051087"
10) "0.17082803611217"

上面的 Ruby 程序每次都只生成同样的列表,用途并不是太大。那么,该怎样修改这个脚本,使得它仍然是一个纯函数(符合 Redis 的要求),但是每次调用都可以产生不同的随机元素呢?

一个简单的办法是,为脚本添加一个额外的参数,让这个参数作为 Lua 的随机数生成器的 seed 值,这样的话,只要给脚本传入不同的 seed ,脚本就会生成不同的列表元素。

以下是修改后的脚本:

RandomPushScript = <<EOF
    local i = tonumber(ARGV[1])
    local res
    math.randomseed(tonumber(ARGV[2]))
    while (i > 0) do
        res = redis.call('lpush',KEYS[1],math.random())
        i = i-1
    end
    return res
EOF

r.del(:mylist)
puts r.eval(RandomPushScript,1,:mylist,10,rand(2**32))

尽管对于同样的 seed ,上面的脚本产生的列表元素是一样的(因为它是一个纯函数),但是只要每次在执行脚本的时候传入不同的 seed ,我们就可以得到带有不同随机元素的列表。

Seed 会在复制(replication link)和写 AOF 文件时作为一个参数来传播,保证在载入 AOF 文件或附属节点(slave)处理脚本时, seed 仍然可以及时得到更新。

注意,Redis 实现保证 math.randommath.randomseed 的输出和运行 Redis 的系统架构无关,无论是 32 位还是 64 位系统,无论是小端(little endian)还是大端(big endian)系统,这两个函数的输出总是相同的。

全局变量保护

为了防止不必要的数据泄漏进 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'

Lua 的 debug 工具,或者其他设施,比如打印(alter)用于实现全局保护的 meta table ,都可以用于实现全局变量保护。

实现全局变量保护并不难,不过有时候还是会不小心而为之。一旦用户在脚本中混入了 Lua 全局状态,那么 AOF 持久化和复制(replication)都会无法保证,所以,请不要使用全局变量。

避免引入全局变量的一个诀窍是:将脚本中用到的所有变量都使用 local 关键字定义为局部变量。

使用脚本散发 Redis 日志

在 Lua 脚本中,可以通过调用 redis.log 函数来写 Redis 日志(log):

redis.log(loglevel, message)

其中, message 参数是一个字符串,而 loglevel 参数可以是以下任意一个值:

  • redis.LOG_DEBUG
  • redis.LOG_VERBOSE
  • redis.LOG_NOTICE
  • redis.LOG_WARNING

上面的这些等级(level)和标准 Redis 日志的等级相对应。

对于脚本散发(emit)的日志,只有那些和当前 Redis 实例所设置的日志等级相同或更高级的日志才会被散发

以下是一个日志示例:

redis.log(redis.LOG_WARNING, "Something is wrong with this script.")

执行上面的函数会产生这样的信息:

[32343] 22 Mar 15:21:39 # Something is wrong with this script.

沙箱(sandbox)和最大执行时间

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

除此之外,脚本还有一个最大执行时间限制,它的默认值是 5 秒钟,一般正常运作的脚本通常可以在几分之几毫秒之内完成,花不了那么多时间,这个限制主要是为了防止因编程错误而造成的无限循环而设置的。

最大执行时间的长短由 lua-time-limit 选项来控制(以毫秒为单位),可以通过编辑 redis.conf 文件或者使用 CONFIG GET parameterCONFIG SET parameter value 命令来修改它。

当一个脚本达到最大执行时间的时候,它并不会自动被 Redis 结束,因为 Redis 必须保证脚本执行的原子性,而中途停止脚本的运行意味着可能会留下未处理完的数据在数据集(data set)里面。

因此,当脚本运行的时间超过最大执行时间后,以下动作会被执行:

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

流水线(pipeline)上下文(context)中的 EVALSHA

在流水线请求的上下文中使用 EVALSHA 命令时,要特别小心,因为在流水线中,必须保证命令的执行顺序。

一旦在流水线中因为 EVALSHA 命令而发生 NOSCRIPT 错误,那么这个流水线就再也没有办法重新执行了,否则的话,命令的执行顺序就会被打乱。

为了防止出现以上所说的问题,客户端库实现应该实施以下的其中一项措施:

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

redis客户端执行Lua脚本

# --eval 后跟文件路径
redis-cli -a 123456 --eval ./Redis_CompareAndSet.lua userName , zhangsan lisi 

--eval而不是命令模式中的"eval",一定要有前端的--脚本路径后紧跟key [key …],相比命令行模式,少了numkeys这个key数量值。key [key …] 和 arg [arg …] 之间的 , ,英文逗号前后必须有空格,否则死活都报错

参考

http://redisdoc.com/script/eval.html

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐