程序员人生 网站导航

redis事物

栏目:互联网时间:2014-11-14 08:43:59

本文档翻译自: http://redis.io/topics/transactions 。

MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事务的基础。

事务可以1次履行多个命令, 并且带有以下两个重要的保证:

  • 事务是1个单独的隔离操作:事务中的所有命令都会序列化、按顺序地履行。事务在履行的进程中,不会被其他客户端发送来的命令要求所打断。

  • 事务是1个原子操作:事务中的命令要末全部被履行,要末全部都不履行。

    EXEC 命令负责触发并履行事务中的所有命令:

    • 如果客户端在使用 MULTI 开启了1个事务以后,却由于断线而没有成功履行 EXEC ,那末事务中的所有命令都不会被履行。
    • 另外一方面,如果客户端成功在开启事务以后履行 EXEC ,那末事务中的所有命令都会被履行。

    当使用 AOF 方式做持久化的时候, Redis 会使用单个 write(2) 命令将事务写入到磁盘中。

    但是,如果 Redis http://www.wfuyu.com/server/由于某些缘由被管理员杀死,或遇上某种硬件故障,那末可能只有部份事务命令会被成功写入到磁盘中。

    如果 Redis 在重新启动时发现 AOF 文件出了这样的问题,那末它会退出,并汇报1个毛病。

    使用 redis-check-aof 程序可以修复这1问题:它会移除 AOF 文件中不完全事务的信息,确保http://www.wfuyu.com/server/可以顺利启动。

从 2.2 版本开始,Redis 还可以通过乐观锁(optimistic lock)实现 CAS (check-and-set)操作,具体信息请参考文档的后半部份。

用法

MULTI 命令用于开启1个事务,它总是返回 OK 。

MULTI 履行以后, 客户端可以继续向http://www.wfuyu.com/server/发送任意多条命令, 这些命令不会立即被履行, 而是被放到1个队列中, 当 EXEC 命令被调用时, 所有队列中的命令才会被履行。

另外一方面, 通过调用 DISCARD , 客户端可以清空事务队列, 并放弃履行事务。

以下是1个事务例子, 它原子地增加了 foo 和 bar 两个键的值:

> MULTI
OK

> INCR foo
QUEUED

> INCR bar
QUEUED

> EXEC
1) (integer) 1
2) (integer) 1

EXEC 命令的回复是1个数组, 数组中的每一个元素都是履行事务中的命令所产生的回复。 其中, 回复元素的前后顺序和命令发送的前后顺序1致。

当客户端处于事务状态时, 所有传入的命令都会返回1个内容为 QUEUED 的状态回复(status reply), 这些被入队的命令将在 EXEC命令被调用时履行。

事务中的毛病

使用事务时可能会遇上以下两种毛病:

  • 事务在履行 EXEC 之前,入队的命令可能会出错。比如说,命令可能会产生语法毛病(参数数量毛病,参数名毛病,等等),或其他更严重的毛病,比如内存不足(如果http://www.wfuyu.com/server/使用 maxmemory 设置了最大内存限制的话)。
  • 命令可能在 EXEC 调用以后失败。举个例子,事务中的命令可能处理了毛病类型的键,比如将列表命令用在了字符串键上面,诸如此类。

对产生在 EXEC 履行之前的毛病,客户端之前的做法是检查命令入队所得的返回值:如果命令入队时返回 QUEUED ,那末入队成功;否则,就是入队失败。如果有命令在入队时失败,那末大部份客户端都会停止并取消这个事务。

不过,从 Redis 2.6.5 开始,http://www.wfuyu.com/server/会对命令入队失败的情况进行记录,并在客户端调用 EXEC 命令时,谢绝履行并自动放弃这个事务。

在 Redis 2.6.5 之前, Redis 只履行事务中那些入队成功的命令,而疏忽那些入队失败的命令。 而新的处理方式则使得在流水线(pipeline)中包括事务变得简单,由于发送事务和读取事务的回复都只需要和http://www.wfuyu.com/server/进行1次通讯。

至于那些在 EXEC 命令履行以后所产生的毛病, 并没有对它们进行特别处理: 即便事务中有某个/某些命令在履行时产生了毛病, 事务中的其他命令依然会继续履行。

从协议的角度来看这个问题,会更容易理解1些。 以下例子中, LPOP 命令的履行将出错, 虽然调用它的语法是正确的:

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

EXEC 返回两条批量回复(bulk reply): 第1条是 OK ,而第2条是 -ERR 。 至于怎样用适合的方法来表示事务中的毛病, 则是由客户端自己决定的。

最重要的是记住这样1条, 即便事务中有某条/某些命令履行失败了, 事务队列中的其他命令依然会继续履行 ―― Redis 不会停止履行事务中的命令。

以下例子展现的是另外一种情况, 当命令在入队时产生毛病, 毛病会立即被返回给客户端:

MULTI
+OK

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

由于调用 INCR 命令的参数格式不正确, 所以这个 INCR 命令入队失败。

为何 Redis 不支持回滚(roll back)

如果你有使用关系式http://www.wfuyu.com/db/的经验, 那末 “Redis 在事务失败时不进行回滚,而是继续履行余下的命令”这类做法可能会让你觉得有点奇怪。

以下是这类做法的优点:

  • Redis 命令只会由于毛病的语法而失败(并且这些问题不能在入队时发现),或是命令用在了毛病类型的键上面:这也就是说,从实用性的角度来讲,失败的命令是由编程毛病酿成的,而这些毛病应当在开发的进程中被发现,而不应当出现在生产环境中。
  • 由于不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。

有种观点认为 Redis 处理事务的做法会产生 bug , 但是需要注意的是, 在通常情况下, 回滚其实不能解决编程毛病带来的问题。 举个例子, 如果你本来想通过 INCR 命令将键的值加上 1 , 却不谨慎加上了 2 , 又或对毛病类型的键履行了 INCR , 回滚是没有办法处理这些情况的。

鉴于没有任何机制能避免http://www.wfuyu.com自己酿成的毛病, 并且这类毛病通常不会在生产环境中出现, 所以 Redis 选择了更简单、更快速的无回滚方式来处理事务。

放弃事务

当履行 DISCARD 命令时, 事务会被放弃, 事务队列会被清空, 并且客户端会从事务状态中退出:

redis> SET foo 1
OK

redis> MULTI
OK

redis> INCR foo
QUEUED

redis> DISCARD
OK

redis> GET foo
"1"

使用 check-and-set 操作实现乐观锁

WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行动。

被 WATCH 的键会被监视,并会发觉这些键是不是被改动过了。 如果有最少1个被监视的键在 EXEC 履行之前被修改了, 那末全部事务都会被取消, EXEC 返回空多条批量回复(null multi-bulk reply)来表示事务已失败。

举个例子, 假定我们需要原子性地为某个值进行增 1 操作(假定 INCR 不存在)。

首先我们可能会这样做:

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

上面的这个实现在只有1个客户真个时候可以履行得很好。 但是, 当多个客户端同时对同1个键进行这样的操作时, 就会产生竞争条件。

举个例子, 如果客户端 A 和 B 都读取了键原来的值, 比如 10 , 那末两个客户端都会将键的值设为 11 , 但正确的结果应当是 12才对。

有了 WATCH , 我们就能够轻松地解决这类问题了:

WATCH mykey

val = GET mykey
val = val + 1

MULTI
SET mykey $val
EXEC

使用上面的代码, 如果在 WATCH 履行以后, EXEC 履行之前, 有其他客户端修改了 mykey 的值, 那末当前客户真个事务就会失败。 程序需要做的, 就是不断重试这个操作, 直到没有产生碰撞为止。

这类情势的锁被称作乐观锁, 它是1种非常强大的锁机制。 并且由于大多数情况下, 不同的客户端会访问不同的键, 碰撞的情况1般都很少, 所以通常其实不需要进行重试。

了解 WATCH

WATCH 使得 EXEC 命令需要有条件地履行: 事务只能在所有被监视键都没有被修改的条件下履行, 如果这个条件不能满足的话,事务就不会被履行。

如果你使用 WATCH 监视了1个带过期时间的键, 那末即便这个键过期了, 事务依然可以正常履行, 关于这方面的详细情况,请看这个帖子: http://code.google.com/p/redis/issues/detail?id=270

WATCH 命令可以被调用屡次。 对键的监视从 WATCH 履行以后开始生效, 直到调用 EXEC 为止。

用户还可以在单个 WATCH 命令中监视任意多个键, 就像这样:

redis> WATCH key1 key2 key3
OK

当 EXEC 被调用时, 不管事务是不是成功履行, 对所有键的监视都会被取消。

另外, 当客户端断开连接时, 该客户端对键的监视也会被取消。

使用无参数的 UNWATCH 命令可以手动取消对所有键的监视。 对1些需要改动多个键的事务, 有时候程序需要同时对多个键进行加锁, 然后检查这些键确当前值是不是符合程序的要求。 当值达不到要求时, 就能够使用 UNWATCH 命令来取消目前对键的监视, 中途放弃这个事务, 并等待事务的下次尝试。

使用 WATCH 实现 ZPOP

WATCH 可以用于创建 Redis 没有内置的原子操作。

举个例子, 以下代码实现了原创的 ZPOP 命令, 它可以原子地弹出有序集合中分值(score)最小的元素:

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

程序只要重复履行这段代码, 直到 EXEC 的返回值不是空多条回复(null multi-bulk reply)便可。

Redis 脚本和事务

从定义上来讲, Redis 中的脚本本身就是1种事务, 所以任何在事务里可以完成的事, 在脚本里面也能完成。 并且1般来讲, 使用脚本要来得更简单,并且速度更快。

由于脚本功能是 Redis 2.6 才引入的, 而事务功能则更早之前就存在了, 所以 Redis 才会同时存在两种处理事务的方法。

不过我们其实不打算在短时间内就移除事务功能, 由于事务提供了1种即便不使用脚本, 也能够避免竞争条件的方法, 而且事务本身的实现其实不复杂。

不过在不远的将来, 可能所有用户都会只使用脚本来实现事务也说不定。 如果真的产生这类情况的话, 那末我们将废弃并终究移除事务功能。

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐