n Lua介绍
Lua是一个高效、简洁、轻量级、可扩展的脚本语言,可以很方便的嵌入到其它语言中使
用,Redis从2.6版支持Lua。
n 使用脚本的好处
1:减少网络开销
2:原子操作:Redis会把脚本当作一个整体来执行,中间不会插入其它命令
3:复用功能
n Lua的使用步骤
1:创建一个.lua结尾的文本文件 例如(test.lua)
2:redis-cli --eval test.lua 执行这个脚本文件
3:如何调试:a.启动redis的时候打开日志输出功能(redis.conf 中配置logfile)
4:在脚本中输出日志,之后就可以在日志文件里使用tail 命令查看打印的日志了。
n Lua的数据类型
Lua是一个动态类型的语言,一个变量可以存储任何类型的值,类型有:
1:空:nil,也就是还没有赋值
2:字符串:用单引号 或者 双引号 引起来
3:数字:包含整数和浮点型
4:布尔:boolean
5:表:表是Lua唯一的数据结构,既可以当数组,也可以做Map,或被视为对象
6:函数:封装某个或某些功能
7:userData:用来将任意 C 数据保存在 Lua 变量中,这样的操作只能通过 C API
8:Thread:用来区别独立的执行线程,它被用来实现 coroutine (协同例程)
n 变量
Lua的变量分成全局变量和局部变量。
1:全局变量
全局变量无需声明即可直接使用,默认值是nil。在Redis脚本中不允许使用全局变量,以
防止脚本之间相互影响。
2:局部变量,声明方法为:local 变量名
3:变量名必须是非数字开头,只能包含字母、数字和下划线,不能是保留关键字,如:
and break do else elseif end false for function if in local nil
not or repeat return then true until while
4:Lua的变量名是区分大小写的
5:局部变量的作用域为从声明开始到所在层的语句块结尾
n 注释
1:单行:--
2:多行:--[[ 开始,到 ]] 结束
n 赋值
Lua支持多重赋值,如:local a,b = 1,2,3
n 操作符
1:数学操作符:+、-、*、/、%、- 取反、^ 幂运算;如果操作数是字符串,会自动转换成数
字进行操作
2:比较操作符:==、~=、〉、>=、<、<=;比较操作符不会转换类型,如果类型不同进行比
较,会返回false;可以手动使用tonumber或者tostring进行转换
3:逻辑操作符:and、or、not
4:连接操作符:..;用来连接两个字符串
5:取长度操作符:#,例如:print(#’helloworld’)
6:操作符的优先级跟其它编程语言是类似的
n If语句
1:格式是:
if 条件 then
elseif 条件 then
else
end
例子:
local a = #"helloworl"
local res
if a%2==0 then
res = 2
elseif a%3==0 then
res = 3
else res = 1
end
2:注意:在Lua中,只有nil和false才是假,其它类型的值均被认为是真
n 循环语句
Lua支持for、while和repeat三种循环语句。
1:for语句格式是:
for 变量=初值,终值,步长 do
end
步长可以省略,默认是1
local num = 0
for i = 1,10,1 do
num = num + i
end
2:增强for循环的格式是:
for 变量1,变量2…,变量N in 迭带器 do
end
local c = {'a','b','c','d'}
for k,v in ipairs(c) do
redis.log(redis.LOG_NOTICE,'k='..k..' v='..v)
end
3:while语句的格式是:
while 条件 do
end
num = 0
local i = 1
while i<=10 do
num = num + i
i = i +1
end
4:repeat语句的格式是:
repeat
until条件
num = 0
i = 1
local sum=1
repeat
sum = sum + 1
i = i + 1
until i > 3
5:使用break来跳出循环块
n 表类型
可以当作数组或者Map来理解,比如:
1:a = {},报一个空表赋值给a
2:a[key]=value,把value赋值给表a中的字段key
3:a={ key1=‘value1’, key2=‘value2’ }
4:引用的时候,可以使用 . 操作符,如: a.key1
5:如果用索引来引用,跟数组是一样的,如:a[1],注意Lua的索引是从1开始
6:可以使用增强for循环来遍历数组,如:
for k,v in ipairs(a) do
print(k)
print(v)
end
local c = {'a','b','c','d'}
for k,v in ipairs(c) do
redis.log(redis.LOG_NOTICE,'k='..k..' v='..v)
end
其中的ipairs是Lua的内置函数,实现类似迭带器的功能,从索引1开始递增遍历到最后一个不为nil的
整数索引。
类似的还有一个pairs,用来便利非数组的表值,它会遍历所有值不为nil的索引。
local d = {id='id1',name='name11'}
for k,v in pairs(d) do
redis.log(redis.LOG_NOTICE,'k='..k..' v='..v)
end
7:也可以使用for循环来按照索引遍历数组,如:
for i=1,#a do
end
local c = {'a','b','c','d'}
for i=1,#c do
redis.log(redis.LOG_NOTICE,'kk='..i..' vv='..c[i])
end
n 函数
1:定义格式为:
function(参数列表)
end
local myAdd = function(a,b)
return a+b
end
local retFun = myAdd(3,5)
redis.log(redis.LOG_NOTICE,'retFun='..retFun)
2:注意:就算没有参数,括号也不能省略
3:形参实参个数不用完全对应,如果想要得到所有的实参,可以把最后一个形参设置成…
local myAdd = function(a,b,...)
local args={...}
for i=1,#args do
redis.log(redis.LOG_NOTICE,'args=='..args[i])
end
return a+b
end
local retFun = myAdd(3,5,8,9,7,6)
4:函数内返回使用return
n Lua的标准库
Lua的标准库提供了很多使用的功能,Redis支持其中大部分,主要有:
1:Base:提供一些基础函数
2:String:提供用于操作字符串的函数
3:Table:提供用于表操作的函数
4:Math:提供数据计算的函数
5:Debug:提供用于调试的函数
n 在Redis中常用的标准库函数
1:string.len(string)
2:string.lower(string)
3:string.upper(string)
4:string.rep(s, n):返回重复s字符串n次的字符串
5:string.sub(string,start[,end]),索引从1开始,-1表示最后一个
6:string.char(n…):把数字转换成字符
7:string.byte (s [, i [, j]]):用于把字符串转换成数字
8:string.find (s, pattern [, init [, plain]]):查找目标模板在给定字符串中出现的位
置,找到返回起始和结束位置,没找到返回nil
9:string.gsub (s, pattern, repl [, n]):将所有符合匹配模式的地方都替换成替代字符
串。并返回替换后的字符串,以及替换次数。四个参数,给定字符串,匹配模式、替代字
符串和要替换的次数
10:string.match (s, pattern [, init]):将返回第一个出现在给定字符串中的匹配字符
串,基本的模式有:. 所有字符,%a字母,%c控制字符,%d数字,%l小写字母,%p 标点符
号字符,%s 空格,%u 大写字母,%w 文字数字字符,%x 16进制数字等
11:string.reverse (s):逆序输出字符串
12:string.gmatch (s, pattern):返回一个迭代器,用于迭代所有出现在给定字符串中的匹
配字符串
local s = string.gmatch("abc23bcbc","%a")
for t in s do
redis.log(redis.LOG_NOTICE,'s=='..t)
end
13:table.concat(table[,sep[,i[,j]]]):将数组转换成字符串,以sep指定的字符串分割,
默认是空,i和j用来限制要转换的表索引的范围,默认是1和表的长度,不支持负索引
local a = {"a1","a2","a3"}
local b = table.concat(a,"--")
redis.log(redis.LOG_NOTICE,'b=='..b)
14:table.insert(table,[pos,]value):向数组中插入元素,pos为指定插入的索引,默认是
数组长度加1,会将索引后面的元素顺序后移
local a = {"a1","a2","a3"}
table.insert(a,2,"a11")
local b = table.concat(a,"--")
redis.log(redis.LOG_NOTICE,'b=='..b)
15:table.remove(table[,pos]):从数组中弹出一个元素,也就是删除这个元素,将后面的
元素前移,返回删除的元素值,默认pos是数组的长度
table.sort(table[,sortFunction]):对数组进行排序,可以自定义排序函数
16:Math库里面常见的:abs、ceil、floor、max、min、pow、sqrt、sin、cos、tan等
17:math.random([m[,n]]):获取随机数,如果是同一个种子的话,每次获得的随机数是一样
的,没有参数,返回0-1的小数;只有m,返回1-m的整数;设置了m和n,返回m-n的整数
18:math.randomseed(x):设置生成随机数的种子
n 其它库
除了标准库外,Redis还会自动加载cjson和cmsgpack库,以提供对Json和MessagePack的支
持,在脚本中分别通过cjson和cmsgpack两个全局变量来访问相应功能
1:cjson.encode(表):把表序列化成字符串
2:cjson.decode(string):把字符串还原成为表
local a = {id=23,name="name",age=23}
local s = cjson.encode(a)
redis.log(redis.LOG_NOTICE,'s=='..s)
local c = cjson.decode(s)
redis.log(redis.LOG_NOTICE,'c.age=='..c.age)
3:cmsgpack.pack(表):把表序列化成字符串
4:cmsgpack.unpack(字符串):把字符串还原成为表
local a = {id=23,name="name",age=23}
local d = cmsgpack.pack(a)
redis.log(redis.LOG_NOTICE,'d=='..d)
local f = cmsgpack.unpack(d)
redis.log(redis.LOG_NOTICE,'id=='..f.id)
n Redis和Lua结合
1:redis.call:在脚本中调用Redis命令,遇到错误会直接返回
redis.call("set","k1","vvv111")
redis.call("set","k2","vvv222")
local d = redis.call("get","k1")
redis.log(redis.LOG_NOTICE,'d=='..d)
local s = redis.call("keys","*")
for i=1,#s do
redis.log(redis.LOG_NOTICE,'key=='..s[i])
end
2:redis.pcall:在脚本中调用Redis命令,遇到错误会记录错误并继续执行
3:Lua数据类型和Redis返回值类型对应
(1)数字——整数
(2)字符串——字符串
(3)表类型——多行字符串
(4)表类型(只有一个ok字段存储状态信息)——状态回复
(5)表类型(只有一个err字段存储错误信息)——错误回复
4:eval命令:在Redis中执行脚本
(1)格式是:eval 脚本内容 key参数数量 [key…] [arg…]
redis-cli --eval test.lua k1 k2 k3 , 1 2 3
(2)通过key和arg两类参数来向脚本传递数据,在脚本中分别用KEYS和ARGV来获取
for i =1,#KEYS do
local s = redis.call("get",KEYS[i]..ARGV[i])
redis.log(redis.LOG_NOTICE,'KEYS'..i..KEYS[i]..' values='..s)
end
for i =1,#ARGV do
redis.log(redis.LOG_NOTICE,'argv=='..ARGV[i])
end
注意:
对于KEYS和ARGV的使用并不是强制的,也可以不从KEYS去获取键,而是在脚本中硬
编码,比如:redis.call(‘get’,’user:’..ARGV[1]) 0 key1 ,照样能取
到”user:key1”对应的值。
但是这种写法,就无法兼容集群,也就是说不能在集群中使用。要兼容集群,建议
的方式是在客户端获取所有的key,然后通过KEYS传到脚本中。
5:evalsha命令:可以通过脚本摘要来运行,其他同eval。执行的时候会根据摘要去找缓存的
脚本,找到了就执行,否则会返回错误。
6:script load:将脚本加入缓存,返回值就是SHA1摘要
7:script exists:判断脚本是否已经缓存
8:script flush:清空脚本缓存
9:script kill:强制终止脚本的执行,如果脚本中修改了某些数据,那么不会终止脚本的执
行,以保证脚本执行的原子性
注意 : 因为redis单线程执行每次提交的命令,所以脚本咋执行的时候是会阻塞其他命令的。 需要对脚本设置最大运行时间。
lua-time-limit:设置ua脚本的最大运行时间,单位是毫秒,如果此值设置为0或负数,则
既不会有报错也不会有时间限制
n 沙箱
为了保证Redis服务器的安全,并且要确保脚本的执行结果只和脚本执行时
传递的参数有关,Redis禁止脚本中使用操作文件或系统调用相关的函数,脚本
中只能对Redis数据进行操作,这就是沙箱。
Redis会禁用脚本的全局变量,以保证脚本之间是隔离的,互不相干的。
n Redis对随机数和随机结果的处理
1:为了确保执行结果可以重现,Redis对随机数的功能进行了处理,以保证每次执
行脚本生成的随机数列都相同
2:Redis还对产生随机结果进行了处理,比如smembers或hkeys等,数据都是无序
的,Redis会对结果按照字典进行顺序排序
3:对于会产生随机结果但无法排序的命令,比如指挥产生一个元素,Redis会在这
类命令执行后,把该脚本标记为lua_random_dirty,此后只允许调用读命令,不
许修改,否则返回错误,这类Redis命令有:spop、srandmember、randomkey、
time。
n MetaTable
用来实现重载操作符功能,基本示例如下:
1:自定义操作的函数,示例:
myAdd={}
function myAdd.__add(f1,f2)
--具体的操作
end
2:为已有的table设置自定义的操作模板,示例:
setmetatable(tableA,myAdd)
setmetatable(tableB,myAdd)
3:对两个table做加的操作,示例:
tableA+tableB
这个时候就会调用自定义的myAdd了,等于重载了默认的_add方法,myAdd的__add方法就是MetaMethod
local score1={yw=80,sx=70}
local score2={yw=90,sx=60}
local myAdd={}
function myAdd.__add(f1,f2)
local ret = {}
ret.yw = f1.yw + f2.yw
ret.sx = f1.sx + f2.sx
return ret
end
setmetatable(score1,myAdd)
setmetatable(score2,myAdd)
local sum = score1 + score2
redis.log(redis.LOG_NOTICE,'yw='..sum.yw..' sx='..sum.sx)
redis.log(redis.LOG_NOTICE,'yw='..sum['yw']..' sx='..sum['sx'])
4:Lua内建约定的MetaMethod :
__add(a, b) 、__sub(a, b)、__mul(a, b)、__div(a, b)、__mod(a, b)、__pow(a, b) 、
__unm(a) 取反、__concat(a, b)、__len(a)、__eq(a, b)、__lt(a, b)、__le(a, b)、__index(a, b)
对应表达式 a.b、__newindex(a, b, c) 对应表达式 a.b = c、__call(a, ...)
n 面向对象
Lua脚本的面向对象类似于JavaScript的面向对象,都是模拟的,比如:
1:直接创建对象:local user={userId='user1',userName='sishuok'}
2:添加新属性:user.age = 12
local score1={yw=80,sx=70}
score1.yy = 100
redis.log(redis.LOG_NOTICE,'score1.yy='..score1.yy)
3:添加方法
function user:show(a)
redis.log(redis.LOG_NOTICE,'a='..a..',age='..self['age'])
end
里面的self就相当于this
local score1={yw=80,sx=70}
local score2={yw=90,sx=60}
function score1:show(a,b)
redis.log(redis.LOG_NOTICE,'这是添加的方法 yy='..score1.yy..' yw='..self.yw)
end
score1:show(1,2)
4:就可以调用方法了:user:show('abc')
5:做个子类来继承user:
local child={address='bj'}
setmetatable(child,{__index=user})
__index在这里起的作用就类似于JS中的Prototype
6:继承了自然就可以调用父类的属性和方法了:child:show('child')
7:当然你还可以定义自己的方法去覆盖父类的方法:
function child:show(a)
redis.log(redis.LOG_NOTICE,'child='..a..',age='..self['age']..',address=='..self.address)
end
local score1={yw=80,sx=70}
score1.yy = 100
function score1:show(a,b)
redis.log(redis.LOG_NOTICE,'这是添加的方法 yy='..score1.yy..' yw='..self.yw)
end
local c = {tw=123}
function c:show(a,b)
redis.log(redis.LOG_NOTICE,'这是子类的方法 父类的yy='..score1.yy..' 父类yw='..self.yw..'自己的tw='..self.tw)
end
setmetatable(c,{__index=score1})
c:show(3,5)
n 模块化
注意:这种方式不能在Redis中使用,目前不支持
1:可以直接使用require(“model_name”)来载入别的lua文件,文件的后缀
是.lua。载入的时候就会直接执行那个文件
2:载入同样的lua文件时,只有第一次的时候会去执行,后面的相同的都不执行了
3:如果要让每一次文件都执行,可使用dofile(“model_name”)函数
4:如果要载入后不执行,等需要的时候执行,可使用 loadfile(“model_name”)
函数,这种是把loadfile的结果赋值给一个变量,比如:
local abc = loadfile(“载入的lua”) 后面需要运行时: abc()