当前位置:网站首页>Redis入门完整教程:事务与Lua
Redis入门完整教程:事务与Lua
2022-07-04 22:29:00 【谷哥学术】
为了保证多条命令组合的原子性,Redis提供了简单的事务功能以及集
成Lua脚本来解决这个问题。本节首先简单介绍Redis中事务的使用方法以及
它的局限性,之后重点介绍Lua语言的基本使用方法,以及如何将Redis和
Lua脚本进行集成,最后给出Redis管理Lua脚本的相关命令。
3.4.1 事务
熟悉关系型数据库的读者应该对事务比较了解,简单地说,事务表示一
组动作,要么全部执行,要么全部不执行。例如在社交网站上用户A关注了
用户B,那么需要在用户A的关注表中加入用户B,并且在用户B的粉丝表中
添加用户A,这两个行为要么全部执行,要么全部不执行,否则会出现数据
不一致的情况。
Redis提供了简单的事务功能,将一组需要一起执行的命令放到multi和
exec两个命令之间。multi命令代表事务开始,exec命令代表事务结束,它们
之间的命令是原子顺序执行的,例如下面操作实现了上述用户关注问题。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> sadd user:a:follow user:b
QUEUED
127.0.0.1:6379> sadd user:b:fans user:a
QUEUED
可以看到sadd命令此时的返回结果是QUEUED,代表命令并没有真正执
行,而是暂时保存在Redis中。如果此时另一个客户端执行sismember user:
a:follow user:b返回结果应该为0。
127.0.0.1:6379> sismember user:a:follow user:b
(integer) 0
只有当exec执行后,用户A关注用户B的行为才算完成,如下所示返回
的两个结果对应sadd命令。
127.0.0.1:6379> exec
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> sismember user:a:follow user:b
(integer) 1
如果要停止事务的执行,可以使用discard命令代替exec命令即可。
127.0.0.1:6379> discard
OK
127.0.0.1:6379> sismember user:a:follow user:b
(integer) 0
如果事务中的命令出现错误,Redis的处理机制也不尽相同。
1.命令错误
例如下面操作错将set写成了sett,属于语法错误,会造成整个事务无法
执行,key和counter的值未发生变化:
127.0.0.1:6388> mget key counter
1) "hello"
2) "100"
127.0.0.1:6388> multi
OK
127.0.0.1:6388> sett key world
(error) ERR unknown command 'sett'
127.0.0.1:6388> incr counter
QUEUED
127.0.0.1:6388> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6388> mget key counter
1) "hello"
2) "100"
2.运行时错误
例如用户B在添加粉丝列表时,误把sadd命令写成了zadd命令,这种就
是运行时命令,因为语法是正确的:
127.0.0.1:6379> multi
OK
127.0.0.1:6379> sadd user:a:follow user:b
QUEUED
127.0.0.1:6379> zadd user:b:fans 1 user:a
QUEUED
127.0.0.1:6379> exec
1) (integer) 1
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> sismember user:a:follow user:b
(integer) 1
可以看到Redis并不支持回滚功能,sadd user:a:follow user:b命令已
经执行成功,开发人员需要自己修复这类问题。
有些应用场景需要在事务之前,确保事务中的key没有被其他客户端修
改过,才执行事务,否则不执行(类似乐观锁)。Redis提供了watch命令来
解决这类问题,表3-2展示了两个客户端执行命令的时序。
可以看到“客户端-1”在执行multi之前执行了watch命令,“客户
端-2”在“客户端-1”执行exec之前修改了key值,造成事务没有执行(exec结果
为nil),整个代码如下所示:
#T1 :客户端 1
127.0.0.1:6379> set key "java"
OK
#T2 :客户端 1
127.0.0.1:6379> watch key
OK
#T3 :客户端 1
127.0.0.1:6379> multi
OK
#T4 :客户端 2
127.0.0.1:6379> append key python
(integer) 11
#T5 :客户端 1
127.0.0.1:6379> append key jedis
QUEUED
#T6 :客户端 1
127.0.0.1:6379> exec
(nil)
#T7 :客户端 1
127.0.0.1:6379> get key
"javapython"
Redis提供了简单的事务,之所以说它简单,主要是因为它不支持事务
中的回滚特性,同时无法实现命令之间的逻辑关系计算,当然也体现了
Redis的“keep it simple”的特性,下一小节介绍的Lua脚本同样可以实现事务
的相关功能,但是功能要强大很多。
3.4.2 Lua用法简述
Lua语言是在1993年由巴西一个大学研究小组发明,其设计目标是作为
嵌入式程序移植到其他应用程序,它是由C语言实现的,虽然简单小巧但是
功能强大,所以许多应用都选用它作为脚本语言,尤其是在游戏领域,例如
大名鼎鼎的暴雪公司将Lua语言引入到“魔兽世界”这款游戏中,Rovio公司将
Lua语言作为“愤怒的小鸟”这款火爆游戏的关卡升级引擎,Web服务器Nginx
将Lua语言作为扩展,增强自身功能。Redis将Lua作为脚本语言可帮助开发
者定制自己的Redis命令,在这之前,必须修改源码。在介绍如何在Redis中
使用Lua脚本之前,有必要对Lua语言的使用做一个基本的介绍。
1.数据类型及其逻辑处理
Lua语言提供了如下几种数据类型:booleans(布尔)、numbers(数
值)、strings(字符串)、tables(表格),和许多高级语言相比,相对简
单。下面将结合例子对Lua的基本数据类型和逻辑处理进行说明。
(1)字符串
下面定义一个字符串类型的数据:
local strings val = "world"
其中,local代表val是一个局部变量,如果没有local代表是全局变量。
print函数可以打印出变量的值,例如下面代码将打印world,其中"--"是Lua
语言的注释。
-- 结果是 "world"
print(hello)
(2)数组
在Lua中,如果要使用类似数组的功能,可以用tables类型,下面代码使
用定义了一个tables类型的变量myArray,但和大多数编程语言不同的是,
Lua的数组下标从1开始计算:
local tables myArray = {"redis", "jedis", true, 88.0}
--true
print(myArray[3])
如果想遍历这个数组,可以使用for和while,这些关键字和许多编程语
言是一致的。
(a)for
下面代码会计算1到100的和,关键字for以end作为结束符:
local int sum = 0
for i = 1, 100
do
sum = sum + i
end
-- 输出结果为 5050
print(sum)
要遍历myArray,首先需要知道tables的长度,只需要在变量前加一个#
号即可:
for i = 1, #myArray
do
print(myArray[i])
end
除此之外,Lua还提供了内置函数ipairs,使用for index,value
ipairs(tables)可以遍历出所有的索引下标和值:
for index,value in ipairs(myArray)
do
print(index)
print(value)
end
(b)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)
(c)if else
要确定数组中是否包含了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
(3)哈希
如果要使用类似哈希的功能,同样可以使用tables类型,例如下面代码
定义了一个tables,每个元素包含了key和value,其中strings1..string2是将两
个字符串进行连接:
local tables user_1 = {age = 28, name = "tome"}
--user_1 age is 28
print("user_1 age is " .. user_1["age"])
如果要遍历user_1,可以使用Lua的内置函数pairs:
for key,value in pairs(user_1)
do print(key .. value)
end
2.函数定义
在Lua中,函数以function开头,以end结尾,funcName是函数名,中间部
分是函数体:
function funcName()
...
end
contact 函数将两个字符串拼接:
function contact(str1, str2)
return str1 .. str2
end
--"hello world"
print(contact("hello ", "world"))
注意
本书只是介绍了Lua部分功能,因为Lua的全部功能已经超出本书的范
围,读者可以购买相应的书籍或者到Lua的官方网站(http://www.lua.org/)
进行学习。
3.4.3 Redis与Lua
1.在Redis中使用Lua
在Redis中执行Lua脚本有两种方法:eval和evalsha。
(1)eval
eval 脚本内容 key 个数 key 列表 参数列表
下面例子使用了key列表和参数列表来为Lua脚本提供更多的灵活性:
127.0.0.1:6379> eval 'return "hello " .. KEYS[1] .. ARGV[1]' 1 redis world
"hello redisworld"
此时KEYS[1]="redis",ARGV[1]="world",所以最终的返回结果
是"hello redisworld"。
如果Lua脚本较长,还可以使用redis-cli--eval直接执行文件。
eval命令和--eval参数本质是一样的,客户端如果想执行Lua脚本,首先
在客户端编写好Lua脚本代码,然后把脚本作为字符串发送给服务端,服务
端会将执行结果返回给客户端,整个过程如图3-7所示。
(2)evalsha
除了使用eval,Redis还提供了evalsha命令来执行Lua脚本。如图3-8所
示,首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和,
evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本,避免每次发送
Lua脚本的开销。这样客户端就不需要每次执行脚本内容,而脚本也会常驻
在服务端,脚本功能得到了复用。
加载脚本:script load命令可以将脚本内容加载到Redis内存中,例如下
面将lua_get.lua加载到Redis中,得到SHA1
为:"7413dc2440db1fea7c0a0bde841fa68eefaf149c"
# redis-cli script load "$(cat lua_get.lua)"
"7413dc2440db1fea7c0a0bde841fa68eefaf149c"
执行脚本:evalsha的使用方法如下,参数使用SHA1值,执行逻辑和
eval一致。
evalsha 脚本 SHA1 值 key 个数 key 列表 参数列表
所以只需要执行如下操作,就可以调用lua_get.lua脚本:
127.0.0.1:6379> evalsha 7413dc2440db1fea7c0a0bde841fa68eefaf149c 1 redis world
"hello redisworld"
2.Lua的Redis API
Lua可以使用redis.call函数实现对Redis的访问,例如下面代码是Lua使用
redis.call调用了Redis的set和get操作:
redis.call("set", "hello", "world")
redis.call("get", "hello")
放在Redis的执行效果如下:
127.0.0.1:6379> eval 'return redis.call("get", KEYS[1])' 1 hello
"world"
除此之外Lua还可以使用redis.pcall函数实现对Redis的调用,redis.call和
redis.pcall的不同在于,如果redis.call执行失败,那么脚本执行结束会直接返
回错误,而redis.pcall会忽略错误继续执行脚本,所以在实际开发中要根据
具体的应用场景进行函数的选择。
开发提示
Lua可以使用redis.log函数将Lua脚本的日志输出到Redis的日志文件中,
但是一定要控制日志级别。
Redis3.2提供了Lua Script Debugger功能用来调试复杂的Lua脚本,具体
可以参考:http://redis.io/topics/ldb。
3.4.4 案例
Lua脚本功能为Redis开发和运维人员带来如下三个好处:
·Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。
·Lua脚本可以帮助开发和运维人员创造出自己定制的命令,并可以将这
些命令常驻在Redis内存中,实现复用的效果。
·Lua脚本可以将多条命令一次性打包,有效地减少网络开销。
下面以一个例子说明Lua脚本的使用,当前列表记录着热门用户的id,
假设这个列表有5个元素,如下所示:
127.0.0.1:6379> lrange hot:user:list 0 -1
1) "user:1:ratio"
2) "user:8:ratio"
3) "user:3:ratio"
4) "user:99:ratio"
5) "user:72:ratio"
user:{id}:ratio代表用户的热度,它本身又是一个字符串类型的键:
127.0.0.1:6379> mget user:1:ratio user:8:ratio user:3:ratio user:99:ratio
user:72:ratio
1) "986"
2) "762"
3) "556"
4) "400"
5) "101"
现要求将列表内所有的键对应热度做加1操作,并且保证是原子执行,
此功能可以利用Lua脚本来实现。
1)将列表中所有元素取出,赋值给mylist:
local mylist = redis.call("lrange", KEYS[1], 0, -1)
2)定义局部变量count=0,这个count就是最后incr的总次数:
local count = 0
3)遍历mylist中所有元素,每次做完count自增,最后返回count:
for index,key in ipairs(mylist)
do
redis.call("incr",key)
count = count + 1
end
return count
将上述脚本写入lrange_and_mincr.lua文件中,并执行如下操作,返回结
果为5。
redis-cli --eval lrange_and_mincr.lua hot:user:list
(integer) 5
执行后所有用户的热度自增1:
127.0.0.1:6379> mget user:1:ratio user:8:ratio user:3:ratio user:99:ratio
user:72:ratio
1) "987"
2) "763"
3) "557"
4) "401"
5) "102"
本节给出的只是一个简单的例子,在实际开发中,开发人员可以发挥自
己的想象力创造出更多新的命令。
3.4.5 Redis如何管理Lua脚本
Redis提供了4个命令实现对Lua脚本的管理,下面分别介绍。
(1)script load
script load script
此命令用于将Lua脚本加载到Redis内存中,前面已经介绍并使用过了,
这里不再赘述。
(2)script exists
scripts exists sha1 [sha1 … ]
此命令用于判断sha1是否已经加载到Redis内存中:
127.0.0.1:6379> script exists a5260dd66ce02462c5b5231c727b3f7772c0bcc5
1) (integer) 1
返回结果代表sha1[sha1…]被加载到Redis内存的个数。
(3)script flush
script flush
此命令用于清除Redis内存已经加载的所有Lua脚本,在执行script flush
后,a5260dd66ce02462c5b5231c727b3f7772c0bcc5不再存在:
127.0.0.1:6379> script exists a5260dd66ce02462c5b5231c727b3f7772c0bcc5
1) (integer) 1
127.0.0.1:6379> script flush
OK
127.0.0.1:6379> script exists a5260dd66ce02462c5b5231c727b3f7772c0bcc5
1) (integer) 0
(4)script kill
script kill
此命令用于杀掉正在执行的Lua脚本。如果Lua脚本比较耗时,甚至Lua
脚本存在问题,那么此时Lua脚本的执行会阻塞Redis,直到脚本执行完毕或
者外部进行干预将其结束。下面我们模拟一个Lua脚本阻塞的情况进行说
明。
下面的代码会使Lua进入死循环:
while 1 == 1
do
end
执行Lua脚本,当前客户端会阻塞:
127.0.0.1:6379> eval 'while 1==1 do end' 0
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的脚本:
127.0.0.1:6379> get hello
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or
SHUTDOWN NOSAVE.
此时Redis已经阻塞,无法处理正常的调用,这时可以选择继续等待,
但更多时候需要快速将脚本杀掉。使用shutdown save显然不太合适,所以选
择script kill,当script kill执行之后,客户端调用会恢复:
127.0.0.1:6379> script kill
OK
127.0.0.1:6379> get hello
"world"
但是有一点需要注意,如果当前Lua脚本正在执行写操作,那么script
kill将不会生效。例如,我们模拟一个不停的写操作:
while 1==1
do
redis.call("set","k","v")
end
此时如果执行script kill,会收到如下异常信息:
(error) UNKILLABLE Sorry the script already executed write commands against the
dataset. You can either wait the script termination or kill the server in a
hard way using the SHUTDOWN NOSAVE command.
上面提示Lua脚本正在向Redis执行写命令,要么等待脚本执行结束要么
使用shutdown save停掉Redis服务。可见Lua脚本虽然好用,但是使用不当破
坏性也是难以想象的。
边栏推荐
- Unity vscode emmylua configuration error resolution
- Redis的持久化机制
- 剑指Offer 68 - II. 二叉树的最近公共祖先
- 【机器学习】手写数字识别
- Is Huatai Securities a nationally recognized securities firm? Is it safe to open an account?
- LOGO特训营 第二节 文字与图形的搭配关系
- Redis sentinel simply looks at the trade-offs between distributed high availability and consistency
- SQL中MAX与GREATEST的区别
- MySQL Architecture - logical architecture
- More than 30 institutions jointly launched the digital collection industry initiative. How will it move forward in the future?
猜你喜欢
Logo Camp d'entraînement section 3 techniques créatives initiales
MySQL Architecture - user rights and management
LOGO特訓營 第一節 鑒別Logo與Logo設計思路
NFT Insider #64:电商巨头eBay提交NFT相关商标申请,毕马威将在Web3和元宇宙中投入3000万美元
Attack and defense world misc advanced area can_ has_ stdio?
Three stage operations in the attack and defense drill of the blue team
UML diagram memory skills
MYSQL架构——用户权限与管理
LOGO特訓營 第三節 首字母創意手法
Naacl-22 | introduce the setting of migration learning on the prompt based text generation task
随机推荐
UML diagram memory skills
[Yugong series] go teaching course 003-ide installation and basic use in July 2022
LOGO特训营 第五节 字体结构与设计常用技法
Unity vscode emmylua configuration error resolution
Feature scaling normalization
leetcode 72. Edit Distance 编辑距离(中等)
不同环境相同配置项的内容如何diff差异?
PostgreSQL server programming aggregation and grouping
剑指 Offer 67. 把字符串转换成整数
Mysql root 账号如何重置密码
剑指 Offer 65. 不用加减乘除做加法
SPSS installation and activation tutorial (including network disk link)
【机器学习】手写数字识别
Co create a collaborative ecosystem of software and hardware: the "Joint submission" of graphcore IPU and Baidu PaddlePaddle appeared in mlperf
The new version judges the code of PC and mobile terminal, the mobile terminal jumps to the mobile terminal, and the PC jumps to the latest valid code of PC terminal
Interview essential leetcode linked list algorithm question summary, whole process dry goods!
Attack and defense world misc advanced area Hong
LOGO特訓營 第一節 鑒別Logo與Logo設計思路
攻防世界 MISC 進階區 Erik-Baleog-and-Olaf
Logo Camp d'entraînement section 3 techniques créatives initiales