Redis 事务:将一组命令放在同一个事务中进行处理

事务

MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事务相关的命令。事务能够一次执行多个命令, 而且带有如下两个重要的保证:html

  • 事务是一个单独的隔离操做:事务中的全部命令都会序列化、按顺序地执行。事务在执行的过程当中,不会被其余客户端发送来的命令请求所打断。redis

  • 事务是一个原子操做:事务中的命令要么所有被执行,要么所有都不执行。数据库

EXEC 命令负责触发并执行事务中的全部命令:编程

  • 若是客户端在使用 MULTI 开启了一个事务以后,却由于断线而没有成功执行 EXEC ,那么事务中的全部命令都不会被执行。
  • 另外一方面,若是客户端成功在开启事务以后执行 EXEC ,那么事务中的全部命令都会被执行。

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

然而,若是 Redis 服务器由于某些缘由被管理员杀死,或者赶上某种硬件故障,那么可能只有部分事务命令会被成功写入到磁盘中。服务器

若是 Redis 在从新启动时发现 AOF 文件出了这样的问题,那么它会退出,并汇报一个错误。google

使用redis-check-aof程序能够修复这一问题:它会移除 AOF 文件中不完整事务的信息,确保服务器能够顺利启动。spa

从 2.2 版本开始,Redis 还能够经过乐观锁(optimistic lock)实现 CAS (check-and-set)操做,具体信息请参考文档的后半部分。code

用法

MULTI 命令用于开启一个事务,它老是返回 OK 。 MULTI 执行以后, 客户端能够继续向服务器发送任意多条命令, 这些命令不会当即被执行, 而是被放到一个队列中, 当 EXEC命令被调用时, 全部队列中的命令才会被执行。htm

另外一方面, 经过调用 DISCARD , 客户端能够清空事务队列, 并放弃执行事务。

如下是一个事务例子, 它原子地增长了 foo 和 bar 两个键的值:

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

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

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

事务中的错误

使用事务时可能会赶上如下两种错误:

  • 事务在执行 EXEC 以前,入队的命令可能会出错。好比说,命令可能会产生语法错误(参数数量错误,参数名错误,等等),或者其余更严重的错误,好比内存不足(若是服务器使用 maxmemory 设置了最大内存限制的话)。
  • 命令可能在 EXEC 调用以后失败。举个例子,事务中的命令可能处理了错误类型的键,好比将列表命令用在了字符串键上面,诸如此类。

对于发生在 EXEC 执行以前的错误,客户端之前的作法是检查命令入队所得的返回值:若是命令入队时返回 QUEUED ,那么入队成功;不然,就是入队失败。若是有命令在入队时失败,那么大部分客户端都会中止并取消这个事务。

不过,从 Redis 2.6.5 开始,服务器会对命令入队失败的状况进行记录,并在客户端调用 EXEC 命令时,拒绝执行并自动放弃这个事务。

在 Redis 2.6.5 之前, Redis 只执行事务中那些入队成功的命令,而忽略那些入队失败的命令。 而新的处理方式则使得在流水线(pipeline)中包含事务变得简单,由于发送事务和读取事务的回复都只须要和服务器进行一次通信。

至于那些在 EXEC 命令执行以后所产生的错误, 并无对它们进行特别处理: 即便事务中有某个/某些命令在执行时产生了错误, 事务中的其余命令仍然会继续执行。

从协议的角度来看这个问题,会更容易理解一些。 如下例子中, 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-string-reply: 第一条是 OK ,而第二条是 -ERR 。 至于怎样用合适的方法来表示事务中的错误, 则是由客户端本身决定的。

最重要的是记住这样一条, 即便事务中有某条/某些命令执行失败了, 事务队列中的其余命令仍然会继续执行 —— Redis 不会中止执行事务中的命令。

如下例子展现的是另外一种状况, 当命令在入队时产生错误, 错误会当即被返回给客户端:

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

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

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

若是你有使用关系式数据库的经验, 那么 “Redis 在事务失败时不进行回滚,而是继续执行余下的命令”这种作法可能会让你以为有点奇怪。

如下是这种作法的优势:

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

有种观点认为 Redis 处理事务的作法会产生 bug , 然而须要注意的是, 在一般状况下, 回滚并不能解决编程错误带来的问题。 举个例子, 若是你原本想经过 INCR 命令将键的值加上 1 , 却不当心加上了 2 , 又或者对错误类型的键执行了 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 返回nil-reply来表示事务已经失败。

举个例子, 假设咱们须要原子性地为某个值进行增 1 操做(假设 INCR 不存在)。

首先咱们可能会这样作:

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

上面的这个实如今只有一个客户端的时候能够执行得很好。 可是, 当多个客户端同时对同一个键进行这样的操做时, 就会产生竞争条件。举个例子, 若是客户端 A 和 B 都读取了键原来的值, 好比 10 , 那么两个客户端都会将键的值设为 11 , 但正确的结果应该是 12 才对。

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

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

使用上面的代码, 若是在 WATCH 执行以后, EXEC 执行以前, 有其余客户端修改了 mykey 的值, 那么当前客户端的事务就会失败。 程序须要作的, 就是不断重试这个操做, 直到没有发生碰撞为止

这种形式的锁被称做乐观锁, 它是一种很是强大的锁机制。 而且由于大多数状况下, 不一样的客户端会访问不一样的键, 碰撞的状况通常都不多, 因此一般并不须要进行重试。

了解 WATCH

WATCH 使得 EXEC 命令须要有条件地执行: 事务只能在全部被监视键都没有被修改的前提下执行, 若是这个前提不能知足的话,事务就不会被执行。 了解更多->

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

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

redis> WATCH key1 key2 key3
OK

当 EXEC 被调用时, 无论事务是否成功执行, 对全部键的监视都会被取消。

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

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

使用 WATCH 实现 ZPOP

WATCH 能够用于建立 Redis 没有内置的原子操做。举个例子, 如下代码实现了原创的 ZPOP 命令, 它能够原子地弹出有序集合中分值(score)最小的元素:

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

程序只要重复执行这段代码, 直到 EXEC 的返回值不是nil-reply回复便可。

Redis 脚本和事务

从定义上来讲, Redis 中的脚本自己就是一种事务, 因此任何在事务里能够完成的事, 在脚本里面也能完成。 而且通常来讲, 使用脚本要来得更简单,而且速度更快

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

不过咱们并不打算在短期内就移除事务功能, 由于事务提供了一种即便不使用脚本, 也能够避免竞争条件的方法, 并且事务自己的实现并不复杂。

不过在不远的未来, 可能全部用户都会只使用脚原本实现事务也说不定。 若是真的发生这种状况的话, 那么咱们将废弃并最终移除事务功能。

相关文章
相关标签/搜索