好哥哥们,
Redis
系列第十三篇,关于Redis
下的Lua
脚本。好吧,我摊牌了。这一篇我是硬写的,是真的硬(没有开车啊)。因为我对 Lua 也不是很熟,然后就在菜鸟教程 一顿操作,还算是入门了。值得一说的就是编程语言之间还是有很多相似的(好哥哥去实践一下就知道了,还是挺好玩的)。
另外的话就是为啥我不懂Lua
还是要写这么一篇,不是欠啊。主要是熟悉这个对后面理解分布式事务框架Redisson
大概的一个逻辑了。还是很有帮助的,看完这篇好哥哥大概就能知道Redisson
大概的一个逻辑了,干货满满。
那像Lua
脚本的安装、配置等相关的基础猛男我就不弄了,好哥哥教程(菜鸟教程,这个名字也是醉了,完全不符合好哥哥(巨佬)身份啊)上都有,当时还是会将一些设计到基础性的,不然跳跃太大,怕好哥哥们受不了啊。
Lua
语言提供了如下几种数据类型:booleans
(布尔)、numbers
(数值)、strings
(字符串)、tables
(表格)。跟我们大 Java 比起来简单太多有没有,下面会对Lua
的基本数据类型和逻辑处理举个栗子(前提是要有Lua
的环境),更多的话就到Lua 官网 或者菜鸟教程学习吧。
-- 进入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
需要注意的是Lua
的数组下标从 1 开始计算
-- 定义一个数组
>local tables myArray = {"redis", "jedis", true, 88.0}
-- 打印输出数组第二个元素
> print(myArray[2])
jedis
如果要使用类似哈希的功能,同样可以使用 tables 类型。下面代码定义了一个tables
,每个元素包含了key
和value
,其中..
是将两个字符串进行连接
-- 定义一个Hash
> local tables user_1 = {age = 27, name = "Dawn"}
-- 打印输出名字
> print("user_1 name is " .. user_1["name"])
user_1 name is Dawn
下面代码会计算 1 到 100 的和,关键字for
以end
作为结束符。
> 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
下面代码同样会计算 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
要确定数组中是否包含了jedis
,有则打印true
,注意if
以end
结尾,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
在Lua
中,函数以function
开头,以end
结尾,funcName
是函数名,中间部分是函数体。
-- 格式
> function funcName()
>> ...
>> end
-- contact 函数将两个字符串拼接
> function contact(str1, str2)
>> return str1 .. str2
>> end
-- 函数调用
> print(contact("hello ", "Dawn"))
hello Dawn
在 Redis 中执行 Lua 脚本有两种方法:eval
和evalsha
。
通过内置的 Lua
解释器,可以使用 EVAL
命令(也可以使用redis-cli
的--eval
参数)对 Lua
脚本进行解析。需要注意的点是执行Lua
也会使Redis
阻塞。
## 格式
eval 脚本内容 key 个数 key 列表 参数列表
## 使用了key列表和参数列表来为Lua脚本提供更多的灵活性
127.0.0.1:6379> eval 'return "hello " .. KEYS[1] .. ARGV[1]' 1 redis world
"hello redisworld"
evalsha
中方式的还就是拆分成两个步骤,首先要将Lua
脚本加载到Redis
服务端,得到该脚本的SHA1
校验码。然后使用evalsha
命令使用SHA1
作为参数可以直接执行对应Lua
脚本。这样做的好处是可以避免每次发送Lua
脚本的开销,而脚本也会常驻在服务端,脚本功能得到了复用。缺点是要怎么管理这些脚本和命令过多的话会占用Redis
的内存。
## 在当前目录定义一个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.call
和 redis.pcall
两个函数实现对Redis
的访问。
redis.call
和redis.pcall
的不同在于,如果redis.call
执行失败,那么脚本执行结束会直接返回错误,而redis.pcall
会忽略错误继续执行脚本。好哥哥们根据实际场景选择对应函数吧。
下面代码使用redis.call
调用了Redis
的set
和get
操作
-- 格式command指的是对应的命令如set/get,后面的话就是key和val
redis.call(command, key, arg)
redis.call("set", "hello", "world")
redis.call("get", "hello")
使用Redis
的eval
执行效果
127.0.0.1:6379> eval 'return redis.call("get", KEYS[1])' 1 hello
"world"
当Lua
脚本存在比较多的逻辑时,显然使用上面的方式明显不合适,这时就有必要单独编写一个Lua
文件。下面的代码逻辑是往Redis
里面设置一个key
和value
,如果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
加载Lua
脚本到Redis
使用script load
,这个上面已经示范过了
## 格式
script load script
使用script exists
判断对应的脚本是否已经加载到 Redis 中,返回结果代表 sha1[sha1…]被加载到 Redis 内存的个数。
## 格式
scripts exists sha1 [sha1 … ]
## 栗子
127.0.0.1:6379> script exists 5ea77eda7a16440abe244e6a88fd9df204ecd5aa
1) (integer) 1
使用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
如果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"
Redisson
原理就是使用Lua
脚本。不熟悉Pipeline
的好哥哥可以看Redis Pipeline 这一篇就够了 。
Pipeline
是非原子性的,Lua
是原子性的。Pipeline
只支持原生命令的打包,而像Lua
的话可以支持复杂的业务逻辑,灵活性更强。Pipeline
不支持集群模式,每次只能作用在一个 Redis 节点上。而Lua
是可以支持集群。Pipeline
与Lua
都不支持事务。Pipeline
一般都是有客户端实时打包命令,而Lua
脚本可以存放于Redis
中,提高复用率。使用Lua
脚本主要有一下好处:
Lua
脚本在Redis
中是原子执行的,执行过程中间不会插入其他命令。上面再讲eval
命令的那张图片就很好的解释了。Lua
脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这些命令常驻在Redis
内存中,实现复用的效果。Lua
脚本可以将多条命令一次性打包,有效地减少网络开销。Lua
脚本可以适配更加复杂的业务场景(比如分布式锁),可嵌入 JAVA,C#等多种编程语言,支持不同操作系统跨平台交互。Lua
简单强大,资源占用率低,支持过程化和对象化的编程语言。Lua
能带来很多优势。但是这也在某种程度上对于好哥哥们的要求更高了,使用Lua
脚本的前提是要熟悉Lua
的基本语法和使用。另外一方面的话,在执行Lua
脚本时,Redis
是阻塞的。所以一方面Lua
和Pipeline
一样,包含的命令都不要太多,另外在Lua
执行的逻辑中也不要有太耗时的业务逻辑,可能会由于Lua
脚本逻辑有问题,产生死循环从而导致整个Redis
服务的不可用。所以Lua
脚本虽然好用,但是使用不当破坏性也是难以想象的,好哥哥们用这之前还是要确保Lua
的正确性。本期就到这啦,有不对的地方欢迎好哥哥们评论区留言,另外求关注、求点赞
上一篇:彻底搞懂 Redis 事务
好哥哥们,
Redis
系列第十三篇,关于Redis
下的Lua
脚本。好吧,我摊牌了。这一篇我是硬写的,是真的硬(没有开车啊)。因为我对 Lua 也不是很熟,然后就在菜鸟教程 一顿操作,还算是入门了。值得一说的就是编程语言之间还是有很多相似的(好哥哥去实践一下就知道了,还是挺好玩的)。
另外的话就是为啥我不懂Lua
还是要写这么一篇,不是欠啊。主要是熟悉这个对后面理解分布式事务框架Redisson
大概的一个逻辑了。还是很有帮助的,看完这篇好哥哥大概就能知道Redisson
大概的一个逻辑了,干货满满。
那像Lua
脚本的安装、配置等相关的基础猛男我就不弄了,好哥哥教程(菜鸟教程,这个名字也是醉了,完全不符合好哥哥(巨佬)身份啊)上都有,当时还是会将一些设计到基础性的,不然跳跃太大,怕好哥哥们受不了啊。
Lua
语言提供了如下几种数据类型:booleans
(布尔)、numbers
(数值)、strings
(字符串)、tables
(表格)。跟我们大 Java 比起来简单太多有没有,下面会对Lua
的基本数据类型和逻辑处理举个栗子(前提是要有Lua
的环境),更多的话就到Lua 官网 或者菜鸟教程学习吧。
-- 进入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
需要注意的是Lua
的数组下标从 1 开始计算
-- 定义一个数组
>local tables myArray = {"redis", "jedis", true, 88.0}
-- 打印输出数组第二个元素
> print(myArray[2])
jedis
如果要使用类似哈希的功能,同样可以使用 tables 类型。下面代码定义了一个tables
,每个元素包含了key
和value
,其中..
是将两个字符串进行连接
-- 定义一个Hash
> local tables user_1 = {age = 27, name = "Dawn"}
-- 打印输出名字
> print("user_1 name is " .. user_1["name"])
user_1 name is Dawn
下面代码会计算 1 到 100 的和,关键字for
以end
作为结束符。
> 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
下面代码同样会计算 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
要确定数组中是否包含了jedis
,有则打印true
,注意if
以end
结尾,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
在Lua
中,函数以function
开头,以end
结尾,funcName
是函数名,中间部分是函数体。
-- 格式
> function funcName()
>> ...
>> end
-- contact 函数将两个字符串拼接
> function contact(str1, str2)
>> return str1 .. str2
>> end
-- 函数调用
> print(contact("hello ", "Dawn"))
hello Dawn
在 Redis 中执行 Lua 脚本有两种方法:eval
和evalsha
。
通过内置的 Lua
解释器,可以使用 EVAL
命令(也可以使用redis-cli
的--eval
参数)对 Lua
脚本进行解析。需要注意的点是执行Lua
也会使Redis
阻塞。
## 格式
eval 脚本内容 key 个数 key 列表 参数列表
## 使用了key列表和参数列表来为Lua脚本提供更多的灵活性
127.0.0.1:6379> eval 'return "hello " .. KEYS[1] .. ARGV[1]' 1 redis world
"hello redisworld"
evalsha
中方式的还就是拆分成两个步骤,首先要将Lua
脚本加载到Redis
服务端,得到该脚本的SHA1
校验码。然后使用evalsha
命令使用SHA1
作为参数可以直接执行对应Lua
脚本。这样做的好处是可以避免每次发送Lua
脚本的开销,而脚本也会常驻在服务端,脚本功能得到了复用。缺点是要怎么管理这些脚本和命令过多的话会占用Redis
的内存。
## 在当前目录定义一个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.call
和 redis.pcall
两个函数实现对Redis
的访问。
redis.call
和redis.pcall
的不同在于,如果redis.call
执行失败,那么脚本执行结束会直接返回错误,而redis.pcall
会忽略错误继续执行脚本。好哥哥们根据实际场景选择对应函数吧。
下面代码使用redis.call
调用了Redis
的set
和get
操作
-- 格式command指的是对应的命令如set/get,后面的话就是key和val
redis.call(command, key, arg)
redis.call("set", "hello", "world")
redis.call("get", "hello")
使用Redis
的eval
执行效果
127.0.0.1:6379> eval 'return redis.call("get", KEYS[1])' 1 hello
"world"
当Lua
脚本存在比较多的逻辑时,显然使用上面的方式明显不合适,这时就有必要单独编写一个Lua
文件。下面的代码逻辑是往Redis
里面设置一个key
和value
,如果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
加载Lua
脚本到Redis
使用script load
,这个上面已经示范过了
## 格式
script load script
使用script exists
判断对应的脚本是否已经加载到 Redis 中,返回结果代表 sha1[sha1…]被加载到 Redis 内存的个数。
## 格式
scripts exists sha1 [sha1 … ]
## 栗子
127.0.0.1:6379> script exists 5ea77eda7a16440abe244e6a88fd9df204ecd5aa
1) (integer) 1
使用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
如果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"
Redisson
原理就是使用Lua
脚本。不熟悉Pipeline
的好哥哥可以看Redis Pipeline 这一篇就够了 。
Pipeline
是非原子性的,Lua
是原子性的。Pipeline
只支持原生命令的打包,而像Lua
的话可以支持复杂的业务逻辑,灵活性更强。Pipeline
不支持集群模式,每次只能作用在一个 Redis 节点上。而Lua
是可以支持集群。Pipeline
与Lua
都不支持事务。Pipeline
一般都是有客户端实时打包命令,而Lua
脚本可以存放于Redis
中,提高复用率。使用Lua
脚本主要有一下好处:
Lua
脚本在Redis
中是原子执行的,执行过程中间不会插入其他命令。上面再讲eval
命令的那张图片就很好的解释了。Lua
脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这些命令常驻在Redis
内存中,实现复用的效果。Lua
脚本可以将多条命令一次性打包,有效地减少网络开销。Lua
脚本可以适配更加复杂的业务场景(比如分布式锁),可嵌入 JAVA,C#等多种编程语言,支持不同操作系统跨平台交互。Lua
简单强大,资源占用率低,支持过程化和对象化的编程语言。Lua
能带来很多优势。但是这也在某种程度上对于好哥哥们的要求更高了,使用Lua
脚本的前提是要熟悉Lua
的基本语法和使用。另外一方面的话,在执行Lua
脚本时,Redis
是阻塞的。所以一方面Lua
和Pipeline
一样,包含的命令都不要太多,另外在Lua
执行的逻辑中也不要有太耗时的业务逻辑,可能会由于Lua
脚本逻辑有问题,产生死循环从而导致整个Redis
服务的不可用。所以Lua
脚本虽然好用,但是使用不当破坏性也是难以想象的,好哥哥们用这之前还是要确保Lua
的正确性。本期就到这啦,有不对的地方欢迎好哥哥们评论区留言,另外求关注、求点赞
上一篇:彻底搞懂 Redis 事务