[译]Redis的事务
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命令单步执行,并提供下面两个重要保证:
- 一个事务中的所有命令串行执行。在事务的执行过程中不会为另一个客户端发起的请求提供服务。这保证了事务中的命令以原子操作的方式被执行。
- 要么全部命令要么没有任何命令被处理。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)。




Recent Comments