Redis Lua
Lua是一个高效的轻量级脚本语言。Lua 在葡萄牙语中是“月亮”的意思,它的徽标形似卫星,寓意着Lua是一个“卫星语言”,能够方便地嵌入到其他语言中使用。
使用脚本的好处
- 减少网络开销:将多个命令合并到一条lua脚本中,发送给redis服务器,可以有效避免因为一次命令发送一次导致的网络开销。
- 原子操作:Redis将整个脚本作为一个整体执行,保证原子性。
- 复用:客户端编写的lua脚本会永久存储到redis中,这也就意味着其他客户端也可以复用这一脚本。
Redis Lua语法
数据类型
类型名 | 取值 |
---|---|
nil | 空类型只包含一个值,即nil,所有没有赋值的变量或表的字段都是nil |
boolean | 布尔类型,取值true和false |
number | 整数 和浮点数都使用数字类型存储 |
string | 字符串,包括单引号和双引号,字符串中也能包含转义字符 |
table | 表类型是lua语言中唯一的数据结构,即可以当数组也可以当字典,十分灵活 |
function | 函数,函数可以作为参数传递给其他函数,也可以作为返回值 |
变量
Lua变量分为局部变量和全局变量,局部变量只在当前函数中有效,全局变量可以在任何地方访问,但是全局变量在多个脚本中可能会冲突,所以不建议使用全局变量,而且全局变量无需声明,可以直接使用,默认值是nil。
a = 1 -- 为全局变量a赋值
print(a) -- 无需声明,直接使用,默认是nil
a = nil -- 删除全局变量a的方法是将其赋值为nil
local b -- 声明一个局部变量b,默认值是nil
local d = 1 -- 声明一个局部变量d,并赋值1
local e, f -- 可以同时声明多个变量
-- 声明一个局部函数
local say_hello = function g() print "hello" end
-- 变量名只能包含字母、数字、下划线,不能包含其他符号,且不能以数字开头,比变量名不能与lua的保留关键字相同
Lua的保留关键字
类别 | 关键字 | 说明 |
---|---|---|
控制结构 | and | 逻辑与运算符 |
or | 逻辑或运算符 | |
not | 逻辑非运算符 | |
if | 条件判断开始 | |
then | 条件判断块的开始 | |
else | 条件判断的替代分支 | |
elseif | 多个条件判断分支 | |
while | 循环结构,当条件为真时重复执行 | |
do | 开始一个代码块 | |
end | 结束一个代码块 | |
repeat | 重复循环,直到条件为真 | |
until | 重复循环的条件判断 | |
for | 循环结构,用于遍历或计数 | |
in | 用于 for 循环中的范围定义 | |
函数和方法 | function | 定义函数 |
return | 返回函数结果 | |
local | 定义局部变量 | |
表和对象 | table | 表类型关键字(虽然不是保留关键字,但在表操作中常用) |
nil | 空值 | |
true | 布尔值真 | |
false | 布尔值假 | |
元表和元方法 | __index | 访问表中不存在的键时调用的方法 |
__newindex | 设置表中不存在的键时调用的方法 | |
__call | 当表被当作函数调用时调用的方法 | |
__metatable | 获取或设置表的元表时调用的方法 | |
__tostring | 将表转换为字符串时调用的方法 | |
__len | 获取表的长度时调用的方法 | |
__pairs | 遍历表时调用的方法 | |
__ipairs | 遍历表索引时调用的方法 | |
__concat | 连接两个表时调用的方法 | |
__add | 加法运算时调用的方法 | |
__sub | 减法运算时调用的方法 | |
__mul | 乘法运算时调用的方法 | |
__div | 除法运算时调用的方法 | |
__mod | 取模运算时调用的方法 | |
__pow | 幂运算时调用的方法 | |
__unm | 取反运算时调用的方法 | |
__eq | 等于比较时调用的方法 | |
__lt | 小于比较时调用的方法 | |
__le | 小于等于比较时调用的方法 | |
其他 | goto | 跳转到标签 |
break | 终止循环或 switch 语句 | |
do | 开始一个代码块 | |
end | 结束一个代码块 |
注释
Lua支持单行注释和块注释,单行注释以--
开头,块注释以--[[
开始,以]]
结束,两者之间可以包含任意多行。
赋值
Lua支持变量赋值,变量赋值语法为:变量名 = 表达式
,变量名可以是一个或多个变量,表达式可以是一个或多个表达式,多个表达式之间用逗号隔开,多个变量之间用逗号隔开。
操作符
Lua有以下5种操作符:
- 算术运算符:
+ - * / % ^
—— 操作符会自动转换数据类型,如数字和字符串相加时,字符串会被转换成数字再进行计算,例如:1 + "2" = 3
- 比较运算符:
== ~= < > <= >=
- 布尔运算符:
and or not
- 连接运算符:
..
- 取长度运算符:
#
—— 例如:#'hello' = 5
各个操作符的优先级如下:
^
(指数)-
(负号,一元运算符)* / %
(乘、除、取余)+ -
(加、减)..
(字符串连接)< > <= >= ~= ==
(关系运算符,不等于和等于)and
or
if语句
在Lua中只有nil
和false
是假值,其余值包括空字符串、0、空表等都是真值。因此判断Redis返回值需要特别注意,否则就容易出错。
Lua和C语言一样每个语句都可以在末尾添加分号,但是一般编写Lua代码时可以省略分号。Lua也不强制要求使用缩进,但是建议使用缩进,这样代码更易读。
循环语句
Lua支持三种循环语句:for
、while
和repeat
。
for循环
for 变量名 = 初始值, 结束值, 步长 do
-- 循环体
end
-- 步长可以省略,默认为1
for i = 1, 10 do
print(i)
end
-- 循环体
end
while循环
while 条件 do
-- 循环体
end
repeat循环
repeat
-- 循环体
until 条件
表类型
表是Lua中唯一的数据结构,可以理解为关联数组,任何类型的值(除了空类型)都可以作为表的索引。
创建一个表
local a = {}
-- b 是一个和传统数组一样的表, b[1] = 1, b[2] = 2, b[3] = 3
local b = {1, 2, 3}
-- 可以使用for语句遍历数组,#b可以获取数组的长度
for i = 1, #b do
print(b[i])
end
-- ipairs是lua提供的一个内置函数,实现迭代器功能,可以遍历数组,返回索引和值
for index, value in ipairs(b) do
print(index, value)
end
-- c 是一个和传统数组一样的表,c[1] = "hello", c[2] = "world"
local c = {"hello", "world"}
-- d 是一个表,d.a = 1, d.b = 2, d.c = 3
local d = {
a = 1,
b = 2,
c = 3
}
-- lua还提供一个迭代器,pairs,可以遍历非数组形式的表,返回键和值
for key, value in pairs(d) do
print(key, value)
end
-- e 是一个表,当访问e.a时,返回"a not found"
local e = setmetatable({}, {
__index = function(t, k)
return k .. " not found"
end
})
print(e.a)
f['field'] = 'value' -- 将field字段的值设置为value
print(f.field) -- 打印内容为value,a.field是a["field"]的语法糖
函数
lua中的函数可以有0个或多个参数,参数以列表的形式提供可以是任意类型。
定义一个函数
-- 定义一个函数
function func(arg1, arg2, ...)
-- 函数体
return value1, value2, ...
end
-- 将函数赋值给变量
local func = function(num)
return num * num
end
-- 函数可以借助lua提供的语法糖简写为
local func
func = function(num)
return num * num
end
标准库
lua提供了丰富的标准库,包括base、string、table、math、debug等,下面简单介绍几个常用的。
string
-- string.len(s)和#的作用类似,返回字符串s的长度
local str = "hello world"
print(string.len(str))
-- 上述代码等价于
print(#str)
-- 转换大小写
print(string.upper(str))
print(string.lower(str))
-- 字符串连接
print(string.format("%s %s", "hello", "world"))
print(string.rep("*", 10))
-- 字符串分割
local str = "hello,world"
local t = string.split(str, ",")
for i, v in ipairs(t) do
print(i, v)
end
-- 获取子字符串
print(string.sub(str, 1, 5))
table
-- table库中大部分函数都需要是数组的形式
local t = {"hello", "world"}
-- 将数组转换成字符串,中间间用逗号分隔
print(table.concat(t, ","))
local t = {1, 2, 3}
-- 排序
table.sort(t)
for i, v in ipairs(t) do
print(i, v)
end
-- 插入元素
table.insert(t, 4)
-- 弹出元素
table.remove(t)
-- 获取元素
print(t[1])
math
lua提供了丰富的数学函数,包括abs、ceil、floor、max、min、mod、random、randomseed、sqrt、type、ult等。
-- math.abs(x)返回x的绝对值
print(math.abs(-1))
-- math.ceil(x)返回大于等于x的最小整数
print(math.ceil(1.1))
-- math.floor(x)返回小于等于x的最大整数
print(math.floor(1.1))
-- math.max(x, y, ...)返回x, y, ...中的最大值
print(math.max(1, 2, 3))
-- math.min(x, y, ...)返回x, y, ...中的最小值
print(math.min(1, 2, 3))
-- math.mod(x, y)返回x除以y的余数
print(math.mod(5, 2))
-- math.random(x)返回[1, x]之间的随机数
print(math.random(10))
-- math.random()
print(math.random())
-- math.randomseed(x)设置随机数种子,如果不设置,每次生成的随机数都是一样的
math.randomseed(os.time())
print(math.random())
-- math.sqrt(x)返回x的平方根
print(math.sqrt(9))
-- math.ult(x, y)返回x是否小于y,x和y都是无符号整数,返回值是布尔值
print(math.ult(1, 2))
其他库
除了lua中的标准库,redis还提供了一些其他库,包括cjson、cmsgpack等,redis会自动加载这这些库,在脚本中可以通过cjson和cmsgpack两个全局变量进行访问。
cjson
cjson提供了json格式的序列化和反序列化功能,使用起来非常方便。
-- 加载 cjson 库
local cjson = require('cjson')
-- 示例 JSON 字符串
local json_str = '{"name": "Alice", "age": 30, "city": "New York"}'
-- 解析 JSON 字符串
local data = cjson.decode(json_str)
-- 修改数据
data.age = 31
-- 生成新的 JSON 字符串
local new_json_str = cjson.encode(data)
-- 将新的 JSON 字符串存储到 Redis 中
redis.call('SET', 'user:alice', new_json_str)
-- 返回新的 JSON 字符串
return new_json_str
redis与lua
编写lua脚本就是为了让redis能够调用,接下来我们看下redis如何与lua脚本交互的。
在脚本中调用redis
在lua脚本中,可以通过redis.call()函数调用redis,第一个参数是redis命令的名称,后面的命令的参数。
例如,我们可以在lua脚本中调用redis的set命令,设置一个键值对。
-- 在lua脚本中调用redis的set命令
redis.call("SET", "key", "value")
-- 在lua脚本中调用redis的get命令
redis.call("GET", "key")
redis命令执行有5种类型回复,redis.call()会将这五种类型回复转换成lua中的类型,具体如下:
redis返回值类型 | lua数据类型 |
---|---|
integer | number |
bulk string | string |
multi bulk string | table - 数组形式 |
status | table (只有一个ok字段存储状态信息) |
error | table (只有一个err字段存储错误信息) |
在很多情况下都需要脚本可以返回值,比如前面的访问频率限制脚本会返回访问频率是否超限。在脚本中可以使用return语句将值返回给客户端,如果没有执行return语句则默认返回nil。因为我们可以像调用其他Redis内置命令一样调用我们自己写的脚本,所以同样Redis会自动将脚本返回值的Lua数据类型转换成Redis的返回值类型。
lua数据类型和redis返回值类型转换表如下:
lua数据类型 | redis返回值类型 |
---|---|
number | integer |
string | bulk string |
table - 数组形式 | multi bulk string |
table (只有一个ok字段存储状态信息) | status |
table (只有一个err字段存储错误信息) | error |
脚本相关命令
编写完lua脚本后,最总要的就是执行,redis提供了EVAL命令,可以使开发者像调用其它redis内置命令一样调用脚本。
EVAL命令的参数如下:
-- 可以通过key和arg参数来向lua脚本传入参数,他们的值可以在脚本中通过KEYS和ARGV两个表类型的全局变量访问。
EVAL script numkeys key [key ...] arg [arg ...]
调用示例:
-- 设置键值对,等价于 SET key hello
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 key hello
-- 获取键值,等价于 GET key
EVAL "return redis.call('GET', KEYS[1])" 1 key
EVAL "local key = KEYS[1]; local value = redis.call('GET', key); if value then return value; else return 'Key does not exist'; end" 1 key
考虑到脚本比较长的情况下,如果每次调用都需要将整个脚本传递个redis会占用较多网络带宽,为了解决这个问题,redis提供了EVALSHA命令,可以先将脚本加载到redis中,然后再通过sha1值调用脚本。
redis在执行EVAL命令时会计算脚本的sha1值,并将值保存在redis中,下次执行EVALSHA命令时,会先根据sha1值查找redis中是否有对应的脚本,如果有则直接执行,否则会报错。
EVALSHA命令的参数如下:
-- 可以通过key和arg参数来向lua脚本传入参数,他们的值可以在脚本中通过KEYS和ARGV两个表类型的全局变量访问。
EVALSHA sha numkeys key [key ...] arg [arg ...]
调用示例:
- 使用
EVAL
命令加载 Lua 脚本:首次运行脚本时,使用EVAL
命令将脚本加载到 Redis 服务器中,并获取脚本的 SHA1 哈希值。 - 使用
EVALSHA
命令执行脚本:后续调用时,使用EVALSHA
命令并通过脚本的 SHA1 哈希值来执行脚本。
假设你有一个 Redis 键 mykey
,并且你想使用 Lua 脚本获取该键的值并打印出来。
使用 SCRIPT
命令加载 Lua 脚本
SCRIPT LOAD "local key = KEYS[1]; local value = redis.call('GET', key); if value then return value; else return 'Key does not exist'; end"
"d3c21d0c2b9ca22f82737626a27bcaf5d288f99f"
使用 EVALSHA
命令执行脚本
-- 假设你已经知道脚本的 SHA1 哈希值,可以直接使用 `EVALSHA` 命令执行脚本。
redis-cli EVALSHA <SHA1_HASH> 1 mykey
-- 使用示例
EVALSHA d3c21d0c2b9ca22f82737626a27bcaf5d288f99f 1 key
"hello"
如果需要再lua脚本中加载脚本,可以使用SCRIPT LOAD命令。
-- 加载脚本
local script_sha = redis.call("SCRIPT", "LOAD", "script")
-- 执行脚本
local result = redis.call("EVAL", script_sha, 1, "key")
-- 返回结果
return result