Home > @Translation > [译]Redis的事务

[译]Redis的事务

December 15th, 2010 西坪 Leave a comment Go to comments

Redis相比于她的一些竞争对手如Memcached, Tokyo Tyrant等,有一个实在是太吸引人的特性,就是事务。尽管它的事务支持不那么完美,但还是很难得。打算介绍一点使用心得,本想先写个简单介绍,不过觉得官方的文档写得相当不错,我索性做个翻译好了。

声明:本文翻译自Redis官方的MULTI/EXEC命令文档。翻译本文旨在传播更多信息,原著版权为Redis所有。转载请包含此声明。

 

WATCH key1 key2 ... keyN (Redis >= 2.1.0)
UNWATCH
MULTI
COMMAND_1 ...
COMMAND_2 ...
COMMAND_N ...
EXEC or DISCARD

MULTI, EXEC, DISCARD 和 WATCH 命令是Redis事务的基石。一个Redis事务允许一组Redis命令单步执行,并提供下面两个重要保证:

  1. 一个事务中的所有命令串行执行。在事务的执行过程中不会为另一个客户端发起的请求提供服务。这保证了事务中的命令以原子操作的方式被执行。
  2. 要么全部命令要么没有任何命令被处理。Exec命令触发事务中的所有命令的执行,所以如果一个在事务上下文中的客户端在调用MULTI(译注:貌似应该是EXEC,原文为MULTI)之前丢失了到服务器的链接,没有任何操作会被执行,而如果EXEC命令被调用了,所有的操作都会执行。一个例外是当AOF开启的时候:每个属于某个Redis事务的命令只要操作完成了的话就会记录在AOF中,所以如果Redis服务器崩溃或者被系统管理员以一种粗暴的方式杀死,可能只有部分操作被登记在AOF中。

从Redis 2.1.0开始,以类似于CAS(比较并设置)操作的乐观地锁定一组key的形式,在上面两种保证之外提供更进一步的保证也是可行的。这将在后文中详细阐述。

用法

通过MULTI命令进入一个事务。MULTI总是返回OK。然后用户可以发起多个命令。Redis将这些命令排队,而不是执行它们。一旦EXEC被调用,所有的命令都被执行。

调用DISCARD则将清空队列并退出事务。

下面是一个使用Ruby客户端的例子:

?> r.multi
=> "OK"
>> r.incr "foo"
=> "QUEUED"
>> r.incr "bar"
=> "QUEUED"
>> r.incr "bar"
=> "QUEUED"
>> r.exec
=> [1, 1, 2]

正如从上面的会话能看出来的那样,MULTI返回一个回复”数组”,每个元素都是事务中的某一个命令的回复,以命令排队时的顺序排列。

当一个Redis连接处于MULTI请求的上下文中时,所有的命令将用一个简单的字符串“QUEUED”回复,如果它们的语法和参数个数都正确的话。某些命令甚至被允许可在执行期间失败。

在协议层面上这更易于理解;在下面的例子中一个命令将在执行时失败,尽管它的语法是正确的:

Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
MULTI
+OK
SET a 3 
abc
+QUEUED
LPOP a
+QUEUED
EXEC
*2
+OK
-ERR Operation against a key holding the wrong kind of value

MULTI 返回一个两个元素的组合回复(bulk reply),其中一个是 +OK码,另一个是 -ERR回复。这中情况下将由客户端库来选择一种合理的方式将错误提供给用户。

重要提示:即使在一个命令将会抛出错误时,队列中的所有其他的命令仍将被处理。在发现错误时,Redis不会停止命令的处理。

这是另一个例子,仍旧使用telnet的写协议,展示了语法错误会被尽可能快地报告:

MULTI
+OK
INCR a b c
-ERR wrong number of arguments for 'incr' command

这次由于语法错误,有问题的INCR命令根本不会被排队。

DISCARD

DISCARD用于放弃一个事务。没有命令将被执行,客户端的状态重新变回正常,在事务之外。这是个使用Ruby客户端的例子:

?> r.set("foo",1)
=> true
>> r.multi
=> "OK"
>> r.incr("foo")
=> "QUEUED"
>> r.discard
=> "OK"
>> r.get("foo")
=> "1"

基于WATCH的比较并设置(CAS)事务

WATCH(观察)用于给Redis事务提供CAS(比较并操作)行为。

调用了WATCH命令(WATCHed)的键将被监视以侦测针对键的修改。如果至少有一个监视的键在EXEC调用之前被修改,整个事务将被放弃,EXEC调用将会返回一个空对象(nil object)(一个Null Multi Bulk回复)来通知客户端事务失败了。

例如想象我们需要给一个键的值原子增一(我知道有INCR,让我们假设我们没有它)。

首先我们可能会像这样尝试:

val = GET mykey
val = val + 1
SET mykey $val

这只在一个给定时间内只有一个客户端做这个操作时可靠地工作。如果多个客户端将在大约同一时间内递增这个键,将会出现竞争条件(race condition)。例如客户端A和B将读到旧值,例如10。这个值将被这两个客户端递增到11,并最终设置这个键的值。于是最后的值将是11,而不是12。

多亏了WATCH,我们能够很好地解决这个问题:

WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC

使用上面的代码,如果存在竞争条件并且另一个客户端在我们调用WATCH和EXEC之间修改了val的结果,事务将会失败。

我们只需要重复这个操作,寄希望于这次没有新的竞争。这种锁定形式叫做乐观锁定,并且在面对许多有多个客户端同时访问数目庞大的键的问题时,是一种十分强大的锁定形式,这种情况下冲突的可能性是很小的:通常操作不需要执行很多次。

WATCH的解释

那WATCH到底是什么呢?它是一个将使得EXEC受到约束的命令:我们要求Redis只在没有别的客户端修改任何一个被监视的键时执行事务。否则,根本就不会开始事务。(注意如果你监视(WATCH)一个瞬时的键,然后Redis在你监视了这个键后,将这个键过期(expire)了,EXEC仍将继续生效。更多信息

WATCH可以被调用很多次。很简单地,所有的WATCH调用都将从调用时开始监视修改,直到EXEC被调用那一刻。

当EXEC被调用时,不管它是成功还是失败,所有的键都不再被监视。同样当一个客户连接被关闭时,所有的key不再被监视。

也可使用UNWATCH命令(没有参数)来清除被监视的键。有时这是有用的:因为可能我们需要执行一个事务来修改一些键,我们乐观锁定了这些键,但是在读取了这些键的当前内容后我们不想继续修改这些键了。这是我们只需要调用UNWATCH,于是这个连接就能够自由地用于新事务了。

WATCH用于实现ZPOP

一个很好的展示WATCH可被用于创建Redis不直接支持的新原子操作的例子是实现ZPOP,这是一个从有序集合(sorted set)中以原子方式弹出(pop)分数权值(score)较低的元素的命令。下面这是最简单的实现:

WATCH zset
ele = ZRANGE zset 0 0
MULTI
ZREM zset ele
EXEC

如果EXEC失败了(返回一个nil值),我们只需要重复上面的操作。

返回值

多组合(Multi bulk)回复, 特别地:

一个MULTI/EXEC命令的返回值是一个多组合回复(multi bulk reply),其中的每个元素是原子事务中的每个命令的返回值。

如果一个MULTI/EXEC事务因为监视命令(WATCH)检测到有键被修改了,返回一个空多组合回复(Null Multi Bulk reply)。

No related posts.

Categories: @Translation Tags: ,