Redis 中的事务

Redis 支持事务,与 redis 的高性能结合起来,使其能完成一些特殊任务,例如全局唯一Id生成器

事务

MULTI, EXEC, DISCARDWATCH 是 Redis 事务的基础。他们允许一次执行一组命令,并有两项重要保证:

  • 在一个事务中的所有命令是连续执行的。在一个 Redis 事务的执行过程中绝不会有另一个客户端发出的请求被处理。这保证了这些命令被作为单一原子操作执行。
  • 要末所有命令都被执行,要末什么都不执行。在事务中,由 EXEC 命令触发执行所有的命令。因此如果一个客户端在事务环境中执行 MULTI 命令之前和服务器的连接中断(译者注:也许这句话里说的应该是 EXEC 命令而不是 MULIT 命令),将不会有任何命令被执行,反之如果 EXEC 命令被调用,则所有操作都会被执行。当使用 append-only file 时,Redis 会使用 a single write(2) syscall 来确保事务写入磁盘。然而如果 Redis 服务器崩溃或者被系统管理员毫不客气的 kill 掉,有可能只有部分操作被登记。Redis 将在启动时检测到这种情况,并退出和返回错误。使用 redis-check-aof 工具有可能修复 append only file,它会删除不完整的事务以便服务器能再次重启。

除了上面两条,Redis 2.2 还提供了一项额外的保证,它以乐观锁定的形提供一种方法,非常类似 check-and-set (CAS) (译者注:条件设置,条件符合则设置,不符合则取消)操作。这在本文后面会讲述。

用法

MULTI 命令进入一个 Redis 事务。这个命令总是返回成功。这时候用户可以提交多个命令。Redis 不会立即执行他们,而是将他们排入队列。一旦调用 EXEC 命令,所有的命令就会被执行。

而调用 DISCARD 将会清空事务的命令队列并退出事务。

下面的例子原子的增加键 foo 和 bar 的值

> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1

从上面的例子中可以看到,MULTI 命令返回一个返回值数组,其中每个元素就是事务中单个命令的返回值,他们的顺序就是命令执行的顺序。

When a Redis connection is in the context of a MULTI request, all commands will reply with the string QUEUED unless they are syntactically incorrect. Some commands are still allowed to fail during execution time.

当一个 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 被用于取消事务。在这种情况下,没有命令会被执行,并且连接的状态会从事务状态恢复到正常状态。

> SET foo 1
OK
> MULTI
OK
> INCR foo
QUEUED
> DISCARD
OK
> GET foo
"1"

用 check-and-set 进行乐观锁定

WATCH 为 Redis 事务提供了 check-and-set (CAS) 特性。

行如其名,WATCH 会监视被其“看管”的每个键。如果在 EXEC 命令之前,有至少一个键被修改了,整个事务将会取消,并且 EXEC 命令返回一个 Null multi-bulk 返回值 来告知客户端事务失败了。

例如,设想我们需要对一个键值加1(假设不存在 INCR 这个方便的命令)

首先会尝试这么做:

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

如果我们只有一个单客户端在做这个操作,那么这段代码是可靠的。如果有多个客户端在几乎相同的时刻试图增加这个键值,会导致竞争状态。例如,客户端 A 和 B 会读取原值,假设原值是 10。经过加法操作,两个客户端都得到 11,最后将 11 SET 为键值。所以最终的键值是 11 而不是 12 。

感谢 WATCH 让我能够非常好的处理此类问题:

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

用上面的代码,如果出现竞争状态:另一个客户端在我们调用 WATCHEXEC 之间的过程中修改了 val 的值,事务将不会执行。

我们只是需要重复这些操作,以期待这次不要再出现新的竞争。这种锁定被称为乐观锁定,这是一种非常激进的锁定形式。在多数情况下,多个客户端会访问不同的键,因此碰撞不会发生,通常不需要重复执行这个操作(译者注:在概率上避免了多次循环造成性能低下或者死锁,如果调用者觉得不安全完全可以自己加计数器防止死锁)。

关于 WATCH 的说明

那么 WATCH 实际上做了什么呢?他会让 EXEC 有附加条件:我们在请求 Redis 完成事务,但仅当没有其他客户端修改任何被 WATCH“监视”的键值时。否则事务将根本不会被进入。(注意:如果 WATCH 一个很容易过期的键,并且在 WATCH 它之后 Redis 就使其期满失效,EXEC 将继续工作。预知详情请点这里。)

WATCH 可以被调用多次。 WATCH 仅在自身调用点和 EXEC 的调用点之间监视键值变化。你也可以向单个 WATCH 语句传递任意数量的键值。

EXEC 被调用后,所有的之前被监视的键值会被取消监视,不管事务是否被取消或者执行。并且当客户端连接丢失的时候,所有东西都会被取消监视。

It is also possible to use the UNWATCH command (without arguments) in order to flush all the watched keys. Sometimes this is useful as we optimistically lock a few keys, since possibly we need to perform a transaction to alter those keys, but after reading the current content of the keys we don’t want to proceed. When this happens we just call UNWATCHso that the connection can already be used freely for new transactions.

为了清除键值的被监视状态,也可以使用 UNWATCH 命令(没有参数)。有时这在乐观锁定某些键时有用,因为有可能我们要在事务中修改这些键值,但读取这些键值的内容后又不想修改了。这种情况下,只需调用 UNWATCH 即可。

WATCH 实现 ZPOP

有个不错的例子,和大家分享一下,演示用 WATCH 如何创建目前不被 Redis 内置支持的原子操作 – 实现 ZPOP,这个命令会原子的从有序集合( sorted set )弹出较低分值(score)的元素。下面是个最简单的实现:

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

如果 EXEC 失败(例如返回 Null multi-bulk reply)我们只需重复这些操作。

(全文完,谢谢阅读!)



Redis

Redis是一种面向“键/值”对类型数据的分布式NoSQL数据库系统,特点是高性能,持久存储,适应高并发的应用场景。它起步较晚,发展迅速,目前已被许多大型机构采用,比如Github,看看谁在用它

本文翻译自Redis的一篇官方文档:Transactions

中英文对照,如有疏漏敬请留言,某些关键词不译,便于阅读。

本文的原始引用地址为:http://blog.1001i.com/2011/10/24/redis-transactions/,译文需不断磨练修订,以日臻完善,因此引用者务必在显著位置保留原始地址,以便读者向译者提建议!