「进击Redis」十三、Redis 万字长文Lua详解

前言

好哥哥们,Redis系列第十三篇,关于Redis下的Lua脚本。好吧,我摊牌了。这一篇我是硬写的,是真的硬(没有开车啊)。因为我对 Lua 也不是很熟,然后就在菜鸟教程 一顿操作,还算是入门了。值得一说的就是编程语言之间还是有很多相似的(好哥哥去实践一下就知道了,还是挺好玩的)。
另外的话就是为啥我不懂Lua还是要写这么一篇,不是欠啊。主要是熟悉这个对后面理解分布式事务框架Redisson大概的一个逻辑了。还是很有帮助的,看完这篇好哥哥大概就能知道Redisson大概的一个逻辑了,干货满满。
那像Lua脚本的安装、配置等相关的基础猛男我就不弄了,好哥哥教程(菜鸟教程,这个名字也是醉了,完全不符合好哥哥(巨佬)身份啊)上都有,当时还是会将一些设计到基础性的,不然跳跃太大,怕好哥哥们受不了啊。
受不了

Lua 基础

Lua语言提供了如下几种数据类型:booleans(布尔)、numbers(数值)、strings(字符串)、tables(表格)。跟我们大 Java 比起来简单太多有没有,下面会对Lua的基本数据类型和逻辑处理举个栗子(前提是要有Lua的环境),更多的话就到Lua 官网 或者菜鸟教程学习吧。

1 字符串

-- 进入Lua
lua -i
-- lua 版本相关
Lua 5.1.4  Copyright (C) 1994-2008 Lua.org, PUC-Rio
-- 交互式执行命令,定义一个字符串类型的数据,其中,local代表val是一个局部变量,如果没有local代表是全局变量。
>local string val = "world"
-- 打印输出
> print(val)
world

2 数组

需要注意的是Lua的数组下标从 1 开始计算

-- 定义一个数组
>local tables myArray = {
    
    "redis", "jedis", true, 88.0}
-- 打印输出数组第二个元素
> print(myArray[2])
jedis

3 哈希

如果要使用类似哈希的功能,同样可以使用 tables 类型。下面代码定义了一个tables,每个元素包含了keyvalue,其中..是将两个字符串进行连接

-- 定义一个Hash
> local tables user_1 = {
    
    age = 27, name = "Dawn"}
-- 打印输出名字
> print("user_1 name is " .. user_1["name"])
user_1 name is Dawn

4 for 循环

下面代码会计算 1 到 100 的和,关键字forend作为结束符。

 > local int sum = 0
 > for i = 1, 100
 >> do
 >> sum = sum + i
 >> end
 --  输出结果为 5050
 > print(sum)
 5050

遍历打印第二个例子中的myArray数组的值

 > for i = 1, #myArray
 >> do
 >> print(myArray[i])
 >> end

5 while 循环

下面代码同样会计算 1 到 100 的和,只不过使用的是while循环,while循环同样以end作为结束符。

 > local int sum = 0
 >> local int i = 0
 >> while i <= 100
 >> do
 >> sum = sum +i
 >> i = i + 1
 >> end
 -- 输出结果为 5050
 > print(sum)
 5050

6 if else

要确定数组中是否包含了jedis,有则打印true,注意ifend结尾,if后紧跟then

> local tables myArray = {
    
    "redis", "jedis", true, 88.0}
>> for i = 1, #myArray
>> do
>> if myArray[i] == "jedis"
>> then
>> print("true")
>> break
>> else
--do nothing
>> end
>> end

7 函数定义

Lua中,函数以function开头,以end结尾,funcName是函数名,中间部分是函数体。

-- 格式
> function funcName()
>> ...
>> end
-- contact 函数将两个字符串拼接
> function contact(str1, str2)
>> return str1 .. str2
>> end
-- 函数调用
> print(contact("hello ", "Dawn"))
hello Dawn

Redis 中使用 Lua

在 Redis 中执行 Lua 脚本有两种方法:evalevalsha

1 eval

通过内置的 Lua 解释器,可以使用 EVAL 命令(也可以使用redis-cli--eval 参数)对 Lua 脚本进行解析。需要注意的点是执行Lua也会使Redis阻塞。
eval

## 格式
eval  脚本内容 key 个数 key 列表 参数列表
## 使用了key列表和参数列表来为Lua脚本提供更多的灵活性
127.0.0.1:6379> eval 'return "hello " .. KEYS[1] .. ARGV[1]' 1 redis world
"hello redisworld"

2 evalsha

evalsha 中方式的还就是拆分成两个步骤,首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验码。然后使用evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本。这样做的好处是可以避免每次发送Lua脚本的开销,而脚本也会常驻在服务端,脚本功能得到了复用。缺点是要怎么管理这些脚本和命令过多的话会占用Redis的内存。
evalsha

## 在当前目录定义一个Lua文件
vim myLua.lua
## 在文件中添加命令并保存
return "hello " .. KEYS[1] .. ARGV[1]
## 1. script load命令可以将脚本内容加载到Redis内存中
redis-cli script load "$(cat myLua.lua)"
## 2. 进入Redis客户端
redis-cli
## 3. evalsha执行脚本格式
evalsha  脚本SHA1值 key个数 key列表 参数列表
## 4. 执行myLua.lua
127.0.0.1:6379> evalsha 5ea77eda7a16440abe244e6a88fd9df204ecd5aa 1 redis world
"hello redisworld"

Lua 的 Redis API

Lua 可以使用redis.callredis.pcall 两个函数实现对Redis的访问。
redis.callredis.pcall的不同在于,如果redis.call执行失败,那么脚本执行结束会直接返回错误,而redis.pcall会忽略错误继续执行脚本。好哥哥们根据实际场景选择对应函数吧。
下面代码使用redis.call调用了Redissetget操作

-- 格式command指的是对应的命令如set/get,后面的话就是key和val
redis.call(command, key, arg)
redis.call("set", "hello", "world")
redis.call("get", "hello")

使用Rediseval执行效果

127.0.0.1:6379> eval 'return redis.call("get", KEYS[1])' 1 hello
"world"

执行 lua 文件

Lua脚本存在比较多的逻辑时,显然使用上面的方式明显不合适,这时就有必要单独编写一个Lua文件。下面的代码逻辑是往Redis里面设置一个keyvalue,如果value等于hello就返回 1,等于world就返回 2

## 1. 还是定义一个lua文件并编辑它
vim myRedisLua.lua
## 2. 添加简单逻辑
redis.call('set', KEYS[1], ARGV[1])
local val = redis.call('get', KEYS[1])
if "hello" == val   then
    return 1
end
if "world" == val then
    return 2
end
## 3. 在redis中执行这个脚本
## 例子,返回1
redis-cli --eval myRedisLua.lua key1 , hello
(integer) 1
## 例子,返回2
redis-cli --eval myRedisLua.lua key1 , world
(integer) 2

Redis 对 Lua 脚本的管理

1 加载 Lua 到 Redis

加载Lua脚本到Redis使用script load,这个上面已经示范过了

## 格式
script load script

2 判断 sha1 在 Redis 是否存在

使用script exists 判断对应的脚本是否已经加载到 Redis 中,返回结果代表 sha1[sha1…]被加载到 Redis 内存的个数。

## 格式
scripts exists sha1 [sha1  … ]
## 栗子
127.0.0.1:6379> script exists 5ea77eda7a16440abe244e6a88fd9df204ecd5aa
1) (integer) 1

3 清除 Redis 内存已加载的所有 Lua 脚本

使用script flush命令来清除 Redis 内存已经加载的所有 Lua 脚本。

## 格式
script flush
## 栗子,先判断脚本是否存在
127.0.0.1:6379> script exists 5ea77eda7a16440abe244e6a88fd9df204ecd5aa
1) (integer) 1
## 执行清除操作
127.0.0.1:6379> script flush
OK
## 再次检查已经不存在了
127.0.0.1:6379> script exists a5260dd66ce02462c5b5231c727b3f7772c0bcc5
1) (integer) 0

4 停止正在执行的 Lua 脚本

如果Lua脚本比较耗时,甚至Lua脚本存在问题,那么此时Lua脚本的执行会阻塞Redis,直到脚本执行完毕或
者外部进行干预将其结束,就可以使用script kill 来杀掉正在执行的 Lua 脚本。
另外Redis提供了一个lua-time-limit参数,默认是 5 秒,它是Lua脚本的“超时时间”,但这个超时时间仅仅是当 Lua 脚本时间超过lua-time-limit后,向其他命令调用发送BUSY的信号,但是并不会停止掉服务端和客户端的脚本执行,所以当达到 lua-time-limit 值之后,其他客户端在执行正常的命令时,将会收到“Busy Redis is busy running a script”错误,并且提示使用script kill或者shutdown nosave命令来杀掉这个 busy 的脚本。下面做个简单的演示

## 写一个死循环的lua脚本并在redis客户端执行
127.0.0.1:6379> eval 'while 1==1 do end' 0
## 重新起一个客户端去执行命令,返回了报错信息,此时Redis已经阻塞,无法处理正常的调用,这时可以选择继续等待。
## 但更多时候需要快速将脚本杀掉。使用shutdown save显然不太合适,所以选择script kill,当script kill执行之后,客户端调用会恢复
127.0.0.1:6379> get test
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
## 另起一个客户端,停止脚本执行
127.0.0.1:6379> script kill
OK
127.0.0.1:6379> get k1
"11"

使用场景

  1. 分布式锁框架Redisson原理就是使用Lua脚本。
  2. 活跃用户判断:判断一个游戏用户是否属于活跃用户,如果符合标准,则活跃用户人数+1。
  3. 简单 DDOS 防护:限制 n 秒内同 IP 的访问次数。
  4. 数据分析,实时平均值统计。
  5. 其他很多具体的业务场景(我没有用过,逻辑判断的数据一般都没有存在 Redis)。

Lua 与 Pipeline 对比

不熟悉Pipeline的好哥哥可以看Redis Pipeline 这一篇就够了

  1. 性能上两种模式都是差不多的,都是将命令一次性打包,从而减少往返时间。
  2. 从原子上来说,Pipeline是非原子性的,Lua是原子性的。
  3. 灵活性上,Pipeline只支持原生命令的打包,而像Lua的话可以支持复杂的业务逻辑,灵活性更强。
  4. Pipeline不支持集群模式,每次只能作用在一个 Redis 节点上。而Lua是可以支持集群。
  5. PipelineLua都不支持事务。
  6. Pipeline一般都是有客户端实时打包命令,而Lua脚本可以存放于Redis中,提高复用率。

总结

使用Lua脚本主要有一下好处:

  1. Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。上面再讲eval命令的那张图片就很好的解释了。
  2. Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这些命令常驻在Redis内存中,实现复用的效果。
  3. Lua脚本可以将多条命令一次性打包,有效地减少网络开销。
  4. Lua脚本可以适配更加复杂的业务场景(比如分布式锁),可嵌入 JAVA,C#等多种编程语言,支持不同操作系统跨平台交互。
  5. 相对来说,Lua简单强大,资源占用率低,支持过程化和对象化的编程语言。
    需要注意的是,Lua能带来很多优势。但是这也在某种程度上对于好哥哥们的要求更高了,使用Lua脚本的前提是要熟悉Lua的基本语法和使用。另外一方面的话,在执行Lua脚本时,Redis是阻塞的。所以一方面LuaPipeline一样,包含的命令都不要太多,另外在Lua执行的逻辑中也不要有太耗时的业务逻辑,可能会由于Lua脚本逻辑有问题,产生死循环从而导致整个Redis服务的不可用。所以Lua脚本虽然好用,但是使用不当破坏性也是难以想象的,好哥哥们用这之前还是要确保Lua的正确性。

本期就到这啦,有不对的地方欢迎好哥哥们评论区留言,另外求关注、求点赞

上一篇:彻底搞懂 Redis 事务

前言

好哥哥们,Redis系列第十三篇,关于Redis下的Lua脚本。好吧,我摊牌了。这一篇我是硬写的,是真的硬(没有开车啊)。因为我对 Lua 也不是很熟,然后就在菜鸟教程 一顿操作,还算是入门了。值得一说的就是编程语言之间还是有很多相似的(好哥哥去实践一下就知道了,还是挺好玩的)。
另外的话就是为啥我不懂Lua还是要写这么一篇,不是欠啊。主要是熟悉这个对后面理解分布式事务框架Redisson大概的一个逻辑了。还是很有帮助的,看完这篇好哥哥大概就能知道Redisson大概的一个逻辑了,干货满满。
那像Lua脚本的安装、配置等相关的基础猛男我就不弄了,好哥哥教程(菜鸟教程,这个名字也是醉了,完全不符合好哥哥(巨佬)身份啊)上都有,当时还是会将一些设计到基础性的,不然跳跃太大,怕好哥哥们受不了啊。
受不了

Lua 基础

Lua语言提供了如下几种数据类型:booleans(布尔)、numbers(数值)、strings(字符串)、tables(表格)。跟我们大 Java 比起来简单太多有没有,下面会对Lua的基本数据类型和逻辑处理举个栗子(前提是要有Lua的环境),更多的话就到Lua 官网 或者菜鸟教程学习吧。

1 字符串

-- 进入Lua
lua -i
-- lua 版本相关
Lua 5.1.4  Copyright (C) 1994-2008 Lua.org, PUC-Rio
-- 交互式执行命令,定义一个字符串类型的数据,其中,local代表val是一个局部变量,如果没有local代表是全局变量。
>local string val = "world"
-- 打印输出
> print(val)
world

2 数组

需要注意的是Lua的数组下标从 1 开始计算

-- 定义一个数组
>local tables myArray = {
    
    "redis", "jedis", true, 88.0}
-- 打印输出数组第二个元素
> print(myArray[2])
jedis

3 哈希

如果要使用类似哈希的功能,同样可以使用 tables 类型。下面代码定义了一个tables,每个元素包含了keyvalue,其中..是将两个字符串进行连接

-- 定义一个Hash
> local tables user_1 = {
    
    age = 27, name = "Dawn"}
-- 打印输出名字
> print("user_1 name is " .. user_1["name"])
user_1 name is Dawn

4 for 循环

下面代码会计算 1 到 100 的和,关键字forend作为结束符。

 > local int sum = 0
 > for i = 1, 100
 >> do
 >> sum = sum + i
 >> end
 --  输出结果为 5050
 > print(sum)
 5050

遍历打印第二个例子中的myArray数组的值

 > for i = 1, #myArray
 >> do
 >> print(myArray[i])
 >> end

5 while 循环

下面代码同样会计算 1 到 100 的和,只不过使用的是while循环,while循环同样以end作为结束符。

 > local int sum = 0
 >> local int i = 0
 >> while i <= 100
 >> do
 >> sum = sum +i
 >> i = i + 1
 >> end
 -- 输出结果为 5050
 > print(sum)
 5050

6 if else

要确定数组中是否包含了jedis,有则打印true,注意ifend结尾,if后紧跟then

> local tables myArray = {
    
    "redis", "jedis", true, 88.0}
>> for i = 1, #myArray
>> do
>> if myArray[i] == "jedis"
>> then
>> print("true")
>> break
>> else
--do nothing
>> end
>> end

7 函数定义

Lua中,函数以function开头,以end结尾,funcName是函数名,中间部分是函数体。

-- 格式
> function funcName()
>> ...
>> end
-- contact 函数将两个字符串拼接
> function contact(str1, str2)
>> return str1 .. str2
>> end
-- 函数调用
> print(contact("hello ", "Dawn"))
hello Dawn

Redis 中使用 Lua

在 Redis 中执行 Lua 脚本有两种方法:evalevalsha

1 eval

通过内置的 Lua 解释器,可以使用 EVAL 命令(也可以使用redis-cli--eval 参数)对 Lua 脚本进行解析。需要注意的点是执行Lua也会使Redis阻塞。
eval

## 格式
eval  脚本内容 key 个数 key 列表 参数列表
## 使用了key列表和参数列表来为Lua脚本提供更多的灵活性
127.0.0.1:6379> eval 'return "hello " .. KEYS[1] .. ARGV[1]' 1 redis world
"hello redisworld"

2 evalsha

evalsha 中方式的还就是拆分成两个步骤,首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验码。然后使用evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本。这样做的好处是可以避免每次发送Lua脚本的开销,而脚本也会常驻在服务端,脚本功能得到了复用。缺点是要怎么管理这些脚本和命令过多的话会占用Redis的内存。
evalsha

## 在当前目录定义一个Lua文件
vim myLua.lua
## 在文件中添加命令并保存
return "hello " .. KEYS[1] .. ARGV[1]
## 1. script load命令可以将脚本内容加载到Redis内存中
redis-cli script load "$(cat myLua.lua)"
## 2. 进入Redis客户端
redis-cli
## 3. evalsha执行脚本格式
evalsha  脚本SHA1值 key个数 key列表 参数列表
## 4. 执行myLua.lua
127.0.0.1:6379> evalsha 5ea77eda7a16440abe244e6a88fd9df204ecd5aa 1 redis world
"hello redisworld"

Lua 的 Redis API

Lua 可以使用redis.callredis.pcall 两个函数实现对Redis的访问。
redis.callredis.pcall的不同在于,如果redis.call执行失败,那么脚本执行结束会直接返回错误,而redis.pcall会忽略错误继续执行脚本。好哥哥们根据实际场景选择对应函数吧。
下面代码使用redis.call调用了Redissetget操作

-- 格式command指的是对应的命令如set/get,后面的话就是key和val
redis.call(command, key, arg)
redis.call("set", "hello", "world")
redis.call("get", "hello")

使用Rediseval执行效果

127.0.0.1:6379> eval 'return redis.call("get", KEYS[1])' 1 hello
"world"

执行 lua 文件

Lua脚本存在比较多的逻辑时,显然使用上面的方式明显不合适,这时就有必要单独编写一个Lua文件。下面的代码逻辑是往Redis里面设置一个keyvalue,如果value等于hello就返回 1,等于world就返回 2

## 1. 还是定义一个lua文件并编辑它
vim myRedisLua.lua
## 2. 添加简单逻辑
redis.call('set', KEYS[1], ARGV[1])
local val = redis.call('get', KEYS[1])
if "hello" == val   then
    return 1
end
if "world" == val then
    return 2
end
## 3. 在redis中执行这个脚本
## 例子,返回1
redis-cli --eval myRedisLua.lua key1 , hello
(integer) 1
## 例子,返回2
redis-cli --eval myRedisLua.lua key1 , world
(integer) 2

Redis 对 Lua 脚本的管理

1 加载 Lua 到 Redis

加载Lua脚本到Redis使用script load,这个上面已经示范过了

## 格式
script load script

2 判断 sha1 在 Redis 是否存在

使用script exists 判断对应的脚本是否已经加载到 Redis 中,返回结果代表 sha1[sha1…]被加载到 Redis 内存的个数。

## 格式
scripts exists sha1 [sha1  … ]
## 栗子
127.0.0.1:6379> script exists 5ea77eda7a16440abe244e6a88fd9df204ecd5aa
1) (integer) 1

3 清除 Redis 内存已加载的所有 Lua 脚本

使用script flush命令来清除 Redis 内存已经加载的所有 Lua 脚本。

## 格式
script flush
## 栗子,先判断脚本是否存在
127.0.0.1:6379> script exists 5ea77eda7a16440abe244e6a88fd9df204ecd5aa
1) (integer) 1
## 执行清除操作
127.0.0.1:6379> script flush
OK
## 再次检查已经不存在了
127.0.0.1:6379> script exists a5260dd66ce02462c5b5231c727b3f7772c0bcc5
1) (integer) 0

4 停止正在执行的 Lua 脚本

如果Lua脚本比较耗时,甚至Lua脚本存在问题,那么此时Lua脚本的执行会阻塞Redis,直到脚本执行完毕或
者外部进行干预将其结束,就可以使用script kill 来杀掉正在执行的 Lua 脚本。
另外Redis提供了一个lua-time-limit参数,默认是 5 秒,它是Lua脚本的“超时时间”,但这个超时时间仅仅是当 Lua 脚本时间超过lua-time-limit后,向其他命令调用发送BUSY的信号,但是并不会停止掉服务端和客户端的脚本执行,所以当达到 lua-time-limit 值之后,其他客户端在执行正常的命令时,将会收到“Busy Redis is busy running a script”错误,并且提示使用script kill或者shutdown nosave命令来杀掉这个 busy 的脚本。下面做个简单的演示

## 写一个死循环的lua脚本并在redis客户端执行
127.0.0.1:6379> eval 'while 1==1 do end' 0
## 重新起一个客户端去执行命令,返回了报错信息,此时Redis已经阻塞,无法处理正常的调用,这时可以选择继续等待。
## 但更多时候需要快速将脚本杀掉。使用shutdown save显然不太合适,所以选择script kill,当script kill执行之后,客户端调用会恢复
127.0.0.1:6379> get test
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN NOSAVE.
## 另起一个客户端,停止脚本执行
127.0.0.1:6379> script kill
OK
127.0.0.1:6379> get k1
"11"

使用场景

  1. 分布式锁框架Redisson原理就是使用Lua脚本。
  2. 活跃用户判断:判断一个游戏用户是否属于活跃用户,如果符合标准,则活跃用户人数+1。
  3. 简单 DDOS 防护:限制 n 秒内同 IP 的访问次数。
  4. 数据分析,实时平均值统计。
  5. 其他很多具体的业务场景(我没有用过,逻辑判断的数据一般都没有存在 Redis)。

Lua 与 Pipeline 对比

不熟悉Pipeline的好哥哥可以看Redis Pipeline 这一篇就够了

  1. 性能上两种模式都是差不多的,都是将命令一次性打包,从而减少往返时间。
  2. 从原子上来说,Pipeline是非原子性的,Lua是原子性的。
  3. 灵活性上,Pipeline只支持原生命令的打包,而像Lua的话可以支持复杂的业务逻辑,灵活性更强。
  4. Pipeline不支持集群模式,每次只能作用在一个 Redis 节点上。而Lua是可以支持集群。
  5. PipelineLua都不支持事务。
  6. Pipeline一般都是有客户端实时打包命令,而Lua脚本可以存放于Redis中,提高复用率。

总结

使用Lua脚本主要有一下好处:

  1. Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。上面再讲eval命令的那张图片就很好的解释了。
  2. Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这些命令常驻在Redis内存中,实现复用的效果。
  3. Lua脚本可以将多条命令一次性打包,有效地减少网络开销。
  4. Lua脚本可以适配更加复杂的业务场景(比如分布式锁),可嵌入 JAVA,C#等多种编程语言,支持不同操作系统跨平台交互。
  5. 相对来说,Lua简单强大,资源占用率低,支持过程化和对象化的编程语言。
    需要注意的是,Lua能带来很多优势。但是这也在某种程度上对于好哥哥们的要求更高了,使用Lua脚本的前提是要熟悉Lua的基本语法和使用。另外一方面的话,在执行Lua脚本时,Redis是阻塞的。所以一方面LuaPipeline一样,包含的命令都不要太多,另外在Lua执行的逻辑中也不要有太耗时的业务逻辑,可能会由于Lua脚本逻辑有问题,产生死循环从而导致整个Redis服务的不可用。所以Lua脚本虽然好用,但是使用不当破坏性也是难以想象的,好哥哥们用这之前还是要确保Lua的正确性。

本期就到这啦,有不对的地方欢迎好哥哥们评论区留言,另外求关注、求点赞

上一篇:彻底搞懂 Redis 事务

猜你喜欢

转载自blog.csdn.net/qq_34090008/article/details/111134945