Redis中如何使用lua脚本-即redis与lua的相互调用

发布于:2024-11-28 ⋅ 阅读:(8) ⋅ 点赞:(0)

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

各个操作符的优先级如下:

  1. ^ (指数)
  2. - (负号,一元运算符)
  3. * / % (乘、除、取余)
  4. + - (加、减)
  5. .. (字符串连接)
  6. < > <= >= ~= == (关系运算符,不等于和等于)
  7. and
  8. or

if语句

在Lua中只有nilfalse是假值,其余值包括空字符串、0、空表等都是真值。因此判断Redis返回值需要特别注意,否则就容易出错。

Lua和C语言一样每个语句都可以在末尾添加分号,但是一般编写Lua代码时可以省略分号。Lua也不强制要求使用缩进,但是建议使用缩进,这样代码更易读。

循环语句

Lua支持三种循环语句:forwhilerepeat

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 ...]

调用示例:

  1. 使用 EVAL 命令加载 Lua 脚本:首次运行脚本时,使用 EVAL 命令将脚本加载到 Redis 服务器中,并获取脚本的 SHA1 哈希值。
  2. 使用 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