redis进阶(二):Lua脚本

前面已经讲过了redis的基础和部分进阶,链接如下:
redis五种数据类型及相关命令
redis进阶:事务|过期|缓存|排序|空间节省
今天我们讲讲redis的脚本,将会让我们更加高效的使用redis

文章目录

    • Lua脚本
      • 数据类型
      • 变量
      • 注释
      • 赋值
      • 操作符
      • if 语句
      • 循环语句
      • 表类型
      • 函数
      • 标准库
    • redis与Lua
      • 客户端调用某脚本
      • 脚本中调用redis命令
      • 从脚本中返回值
      • 脚本相关命令
    • 写在最后

Lua脚本

redis在2.6版本推出了脚本功能,允许开发者使用Lua语言编写脚本传到redis中执行,在使用脚本之前,我们首先需要了解为什么要使用脚本,使用脚本的好处主要包括以下三点:

  1. 减少网络开销,如果一个功能需要多次请求redis,使用脚本完成同样的操作只需要请求一次,减少了网络往返时延
  2. 原子操作,redis会将脚本作为一个整体执行,中间不会被其他命令插入,无需担心竞态条件,无需使用事务,事务可以完成的所有功能都可以用脚本实现
  3. 复用,客户端发送的脚本会永久存储在redis中,其他客户端可以复用这一脚本

Lua语言简介:Lua是一个高效的轻量级脚本语言,能够方便的嵌入到其他语言中,使得程序的升级和扩展变得更容易,redis使用Lua5.1版本,所以以下语法基于此版本,且着重介绍编写redis脚本会用到的部分

数据类型

Lua是一个动态类型语言,一个变量可以存储任何类型的值,编写redis脚本时会用到的类型如下:
redis进阶(二):Lua脚本_第1张图片

变量

Lua的变量分为全局变量和局部变量,全局变量无需声明就可以使用,默认值是 nil
变量名必须是非数字开头,只能包含字母,数字和下划线,区分大小写,且不能与Lua的保留关键字相同
全局变量的声明:

a = 1		--为全局变量a赋值
print(b)	--无需声明即可使用,默认值是 nil
a = nil		--删除全局变量a的方法是赋值 nil ,全局变量没有声明和未声明之分,只有 非nil 和 nil 的区别

在redis脚本中不能使用全局变量,只允许使用局部变量以防止脚本之间相互影响

局部变量的声明:

local c			--声明一个局部变量 c ,默认值是 nil
local d = 1		--声明一个局部变量 d 并赋值为 1
local e, f		--可以同时声明多个局部变量

存储函数的局部变量声明:

local say_hi = function ()
    print 'hi'
end

作用域:
局部变量的作用域从声明开始到所在层的语句块末尾,比如:

local x = 10
if true then
	local x = x + 1
	print(x)
	do
		local x = x + 1
		pring(x)
	end
	print(x)
end
print(x)
//打印结果为:
11
12
11
10

注释

-- 这是一个单行注释,以"--"开始,行尾结束,一般习惯在"--"后面跟上一个空格
--[[
这是一个多行注释
以"--[["开始, 到"]]"结束
]]

赋值

Lua支持多重赋值,比如:

local a, b = 1, 2		-- a的值是1, b的值是2
local c, d = 1, 2, 3	-- c的值是1, d的值是2, 3被舍弃了
local e, f = 1			-- e的值是1, f的值是nil

执行多重赋值前,Lua会先计算所有表达式的值,比如:

local a = {1, 2, 3}
local i = 1
i, a[i] = i+1, 5	-- 等价于: i, a[1] = 2, 5
-- 结果: i的值为2, a则为{5, 2, 3}
-- lua表类型索引是从1开始的

操作符

Lua有下面5类操作符:

  1. 数学操作符,常见的有: +、 -、 *、 /、 %(取模)、 ^(幂运算) ; 操作字符串会自动转换成数字,比如 print(‘1’+1) – 打印2
  2. 比较操作符,包括: ==(比较两个操作数的类型和值是否都相等)、 ~=(于==结果相反)、 <、 >、 <=、 >= ; 比较操作符不会对两边操作数进行自动类型转换
  3. 逻辑操作符,redis进阶(二):Lua脚本_第2张图片
    Lua逻辑操作运算符支持短路
    只要操作数不是nil或false,就认为操作数为真(包含0或空字符串),例如:
print(1 and 5)		-- 5
print(1 or 5)		-- 1
print(not 0)		-- false
print('' or 1)		-- ''
  1. 连接操作符,只有一个: .. ,用来连接两个字符串,自动将数字转换为字符串,例如:
print('this price is' .. 25)	-- 'this price is 25'
  1. 取长度操作符,同样只有一个: # , 用来获取字符串或表长度:
print(#'hello')		-- 5

运算符优先级如下,依次降低:

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

if 语句

if 条件表达式 then
	语句块
elseif 条件表达式 then
	语句块
else
	语句块
end

Lua每个语句后面都可以;结尾,但一般都会省略
也不强制要求缩进,所有代码都可以写在同一行,但为了可读性,编写时一定要注意缩进

循环语句

Lua支持while,repeat和for循环

while 条件表达式 do
	语句块
end

repeat
	语句块
until 条件表达式

for 变量 = 初值, 终值, 步长 do
	语句块
end

for循环步长可以省略,默认为1,如计算1-100的和:

--其中i虽然没用local声明,但i是局部变量
local sum = 0
for i = 1, 100 do
	sum = sum + i
end

表类型

表是Lua唯一的数据结构,可以理解为关联数组,任何类型的值(除了空类型)都可以作为表的索引
Lua约定数组的索引是从1开始的,而不是0
表的定义:

-- 第一种,类似于java Map
a = {}					-- 将变量a赋值一个空表
a['field'] = 'value'	-- 将field字段赋值value
print(a.field)			--打印结果为 'value'
-- 或者这样定义:
people = {
	name = 'Bob'
	age = 20
}
print(people.name)		-- 打印结果为: 'Bob'
-- 遍历非数组的表值:
for index, value in pairs(people) do
	print(index)
	print(value)
end
-- 打印结果:
name
Bob
age
20
-- 第二种,类似于java List
a = {}
a[1] = 'Bob'
a[1] = 'Jeff'
-- 或者:
a = {'Bob', 'Jeff'}
print(a[1])				--打印结果为: 'Bob'
-- 遍历方式:
for index, value in ipairs(a) do
	print(index)			--迭代数组 a 的索引
	print(value)			--迭代数组 a 的值
end
-- 或者:
for i = 1,#a do
	print(i)
	print(a[i])
end
-- 打印结果都为:
1
Bob
2
Jeff

pairs于ipairs都是Lua的迭代器,区别在于前者会遍历所有值不为nil的索引,而后者只会从索引1开始递增遍历到最后一个值不为nil的整数索引

函数

函数的定义:

local 局部变量名 = function (参数列表)
	函数体
end
-- 简化版定义:
local function 局部变量名 (参数列表)
	函数体
end
-- 例:
local function square (num)
	return num * num
end
-- 可变参数个数,将最后一个形参写为: ...  ,比如希望传入若干参数计算这些数的平方:
local function square (...)
	loca argv = {...}
	for i = 1, #argv do
		argv[i] = argv[i] * argv[i]
	end
	return unpack(argv)		--unpack函数用来返回表中元素,相当于return argv[1], argv[2], argv[3]
end

a, b, c = square(1, 2, 3)
print(a)
print(b)
print(c)
-- 打印结果:
1
4
9

Lua中return和break语句必须是语句块的最后一条语句,即后面只能是end,else或util,如果需要在语句块中间使用,必须人为的使用do和end将其包围

标准库

redis进阶(二):Lua脚本_第3张图片

redis与Lua

客户端调用某脚本

将脚本存为 ratelimiting.lua
在命令行输入 redis-cli --eval /path/to/ratelimiting.lua rate.limiting:127.0.0.1 , 10 3
参数说明:

  1. –eval 告诉 redis-cli 读取并运行后面的Lua脚本
  2. /path/to/ratelimiting.lua 是 ratelimiting.lua 文件的位置,后面是传给脚本的参数
  3. , 前的 rate.limiting:127.0.0.1 是要操作的键,可以在脚本中使用 KEYS[1] 获取
  4. , 后面的 10 和 3 是参数,在脚本中能够使用 ARGV[1] 和 ARGV[2] 获得

tips: , 两边的空格不能省略,否则报错

脚本中调用redis命令

redis.call('set', 'foo', 'bar')
local value = redis.call('get', 'foo')		--value的值为bar

redis.call函数会将5种类型的返回值转换为Lua对应数据类型(空结果特殊,对应Lua的false),具体对应关系:
redis进阶(二):Lua脚本_第4张图片
还提供了redis.pcall函数,功能与redis.call相同,唯一区别是当命令出错时,redis.pcall记录错误继续执行,redis.call直接返回错误,不会继续执行

从脚本中返回值

脚本中使用return语句将值返回给客户端,如果没有执行return语句则默认返回nil,同样redis会将脚本返回值的Lua数据类型转换成redis的返回值类型(Lua的false特殊,会被转换成空结果)
redis进阶(二):Lua脚本_第5张图片

脚本相关命令

EVAL命令
格式: EVAL 脚本内容 key参数个数 [key…] [arg…]
key参数个数不能省略,无参则设为0
例如实现SET命令:
脚本内容:
return reids.call(‘SET’, KEYS[1], ARGV[1])
现在打开redis-cli执行脚本:

redis> EVAL "return reids.call('SET', KEYS[1], ARGV[1])" 1 foo bar
OK
redis> GET foo
"bar"

其中要读写的键名应该作为key参数,其他的作为arg参数,否则无法兼容集群

EVALSHA命令
如果脚本较长,每次传整个脚本占用较多带宽,则使用该命令
执行EVAL命令时redis会计算脚本的SHA1摘要并记录在脚本缓存中,执行EVALSHA命令时redis会根据提供的摘要从缓存中查找对应脚本内容,如果找到就执行,否则返回"NOSCRIPT No matching script.Please use EVAL"
用法与EVAL一样,只不过将脚本内容替换成脚本内容的SHA1摘要
一般使用该命令流程为:

  1. 先计算脚本的SHA1摘要,并使用EVALSHA命令执行脚本
  2. 获得返回值,如果返回"NOSCRIPT"错误则使用EVAL重新执行脚本

虽然麻烦,但是很多编程语言的redis客户端封装了该操作

写在最后

redis执行脚本期间不会执行其他命令,如果脚本执行时间过长将导致redis无法提供服务,所以可以通过配置 lua-time-limit 参数限制脚本运行的最长时间,默认5秒,
超过这个时间后redis将开始接收其他命令但不执行,而是返回busy错误,
此时redis实际会执行的只有两个命令: SCRIPT KILLSHUTDOWN NOSAVE,
如果脚本已经对reids数据进行了修改,SCRIPT KILL会返回错误,无法终止脚本,此时只能使用SHUTDOWN NOSAVE命令,但该命令不会做持久化操作,这意味着发生在上一次快照后的数据库修改都会丢失

你可能感兴趣的:(redis进阶(二):Lua脚本)