Redis开发与运维. 3.4 事务与Lua

3.4 事务与Lua

为了保证多条命令组合的原子性,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展示了两个客户端执行命令的时序。

表3-2 事务中watch命令演示时序

时间点     客户端-1 客户端-2

T1     set
key "java" 

T2     watch
key        

T3     multi        

T4              append
key python

T5     append
key jedis     

T6     exec

T7     get
key    

 

可以看到“客户端-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所示。

 

图3-7 eval命令执行Lua脚本过程

(2)evalsha

除了使用eval,Redis还提供了evalsha命令来执行Lua脚本。如图3-8所示,首先要将Lua脚本加载到Redis服务端,得到该脚本的SHA1校验和,evalsha命令使用SHA1作为参数可以直接执行对应Lua脚本,避免每次发送Lua脚本的开销。这样客户端就不需要每次执行脚本内容,而脚本也会常驻在服务端,脚本功能得到了复用。

 

图3-8 使用evalsha执行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的日志文件中,但是一定要控制日志级别。

Redis 3.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脚本虽然好用,但是使用不当破坏性也是难以想象的。

时间: 2024-09-20 00:07:51

Redis开发与运维. 3.4 事务与Lua的相关文章

Redis开发与运维. 导读

数据库技术丛书 Redis开发与运维 付磊 张翼军编著   Redis作为基于键值对的NoSQL数据库,具有高性能.丰富的数据结构.持久化.高可用.分布式等特性,同时Redis本身非常稳定,已经得到业界的广泛认可和使用.掌握Redis已经逐步成为开发和运维人员的必备技能之一. 本书关注了Redis开发运维的方方面面,尤其对于开发运维中如何提高效率.减少可能遇到的问题进行详细分析,但本书不单单介绍怎么解决这些问题,而是通过对Redis重要原理的解析,帮助开发运维人员学会找到问题的方法,以及理解背后

Redis开发与运维. 3.2 Redis Shell

3.2 Redis Shell Redis提供了redis-cli.redis-server.redis-benchmark等Shell工具.它们虽然比较简单,但是麻雀虽小五脏俱全,有时可以很巧妙地解决一些问题. 3.2.1 redis-cli详解 第1章曾介绍过redis-cli,包括-h.-p参数,但是除了这些参数,还有很多有用的参数,要了解redis-cli的全部参数,可以执行redis-cli -help命令来进行查看,下面将对一些重要参数的含义以及使用场景进行说明. 1.?-r -r(

Redis开发与运维. 1.2 Redis特性

1.2 Redis特性 Redis之所以受到如此多公司的青睐,必然有之过人之处,下面是关于Redis的8个重要特性. 1.?速度快 正常情况下,Redis执行命令的速度非常快,官方给出的数字是读写性能可以达到10万/秒,当然这也取决于机器的性能,但这里先不讨论机器性能上的差异,只分析一下是什么造就了Redis除此之快的速度,可以大致归纳为以下四点: Redis的所有数据都是存放在内存中的,表1-1是谷歌公司2009年给出的各层级硬件执行速度,所以把数据放在内存中是Redis速度快的最主要原因.

Redis开发与运维. 2.5 集合

2.5 集合 集合(set)类型也是用来保存多个的字符串元素,但和列表类型不一样的是,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素.如图2-22所示,集合user:1:follow包含着"it"."music". "his"."sports"四个元素,一个集合最多可以存储232-1个元素.Redis除了支持集合内的增删改查,同时还支持多个集合取交集.并集.差集,合理地使用好集合类型,能在实际开发中

Redis开发与运维. 1.1 盛赞Redis

1.1 盛赞Redis Redis是一种基于键值对(key-value)的NoSQL数据库,与很多键值对数据库不同的是,Redis中的值可以是由string(字符串).hash(哈希).list(列表).set(集合).zset(有序集合).Bitmaps(位图).HyperLogLog.GEO(地理信息定位)等多种数据结构和算法组成,因此Redis可以满足很多的应用场景,而且因为Redis会将所有数据都存放在内存中,所以它的读写性能非常惊人.不仅如此,Redis还可以将内存的数据利用快照和日志

Redis开发与运维. 2.1 预备

2.1 预备 在正式介绍5种数据结构之前,了解一下Redis的一些全局命令.数据结构和内部编码.单线程命令处理机制是十分有必要的,它们能为后面内容的学习打下一个好的基础,主要体现在两个方面:第一.Redis的命令有上百个,如果纯靠死记硬背比较困难,但是如果理解Redis的一些机制,会发现这些命令有很强的通用性.第二.Redis不是万金油,有些数据结构和命令必须在特定场景下使用,一旦使用不当可能对Redis本身或者应用本身造成致命伤害. 2.1.1 全局命令 Redis有5种数据结构,它们是键值对

Redis开发与运维. 2.7 键管理

2.7 键管理 本节将按照单个键.遍历键.数据库管理三个维度对一些通用命令进行介绍. 2.7.1 单个键管理 针对单个键的命令,前面几节已经介绍过一部分了,例如type.del.object.exists.expire等,下面将介绍剩余的几个重要命令. 1.?键重命名 rename key newkey 例如现有一个键值对,键为python,值为jedis: 127.0.0.1:6379> get python "jedis" 下面操作将键python重命名为java: 127.

Redis开发与运维. 3.1 慢查询分析

3.1 慢查询分析 许多存储系统(例如MySQL)提供慢查询日志帮助开发和运维人员定位系统存在的慢操作.所谓慢查询日志就是系统在命令执行前后计算每条命令的执行时间,当超过预设阀值,就将这条命令的相关信息(例如:发生时间,耗时,命令的详细信息)记录下来,Redis也提供了类似的功能. 如图3-1所示,Redis客户端执行一条命令分为如下4个部分:   图3-1 一条客户端命令的生命周期 1)发送命令 2)命令排队 3)命令执行 4)返回结果 需要注意,慢查询只统计步骤3)的时间,所以没有慢查询并不

Redis开发与运维. 2.8 本章重点回顾

2.8 本章重点回顾 1)Redis提供5种数据结构,每种数据结构都有多种内部编码实现. 2)纯内存存储.IO多路复用技术.单线程架构是造就Redis高性能的三个因素. 3)由于Redis的单线程架构,所以需要每个命令能被快速执行完,否则会存在阻塞Redis的可能,理解Redis单线程命令处理机制是开发和运维Redis的核心之一. 4)批量操作(例如mget.mset.hmset等)能够有效提高命令执行的效率,但要注意每次批量操作的个数和字节数. 5)了解每个命令的时间复杂度在开发中至关重要,例