*Redis事务机制详解

*相关命令

*事务

Redis使用MULTIEXECDISCARD 和 WATCH 命令来实现事务功能。事务可以一次执行多个命令,并带有两个重要的保证: 

  • 事务中的所有命令都被序列化并按顺序执行。Redis执行事务期间,不会被其它客户端发送的命令打断,事务中的所有命令都作为一个隔离操作顺序执行。

  • Redis事务是原子操作,或者执行所有命令或者都不执行。 EXEC 命令触发一个事务中所有命令的执行,所以,如果一个客户端断在调用EXEC 命令前丢失连接,那么所有的命令不会被执行,相反,如果EXEC 被调用,那么所有命令会被执行。当使用 append-only file 方式持久化时,Redis使用单个 write(2) 系统调用将事务写到磁盘上。但是,如果Redis服务器崩溃或被系统管理员以某种硬方式杀死,则可能只注册了部分操作。Redis重启的时候会检测到这种情况,并返回错误退出。使用 redis-check-aof 工具可以删除部分事务,这样Redis可以重新启动。

从2.2起,Redis提供了额外的保证,以类似check-and-set (CAS)的乐观锁形式 。后面会详细介绍。

*用法

Redis使用 MULTI 命令标记事务开始,它总是返回OK。MULTI执行之后,客户端可以发送多条命令,Redis会把这些命令保存在队列当中,而不是立刻执行这些命令。所有的命令会在调用EXEC 命令之后执行。 

如果不调用EXEC,调用 DISCARD 会清空事务队列并退出事务。

以下是一个事务例子, 它原子地增加了 foo{.highlighter-rouge} 和 bar{.highlighter-rouge} 两个键的值。

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

从上面例子可以看出, EXEC 返回一个应答数组,数组中的每个元素对应着事务中的一个命令,和命令发送的顺序一致。

当Redis连接执行MULTI 命令后,之后所有的命令返回字符串 QUEUED 。入队的命令将在 EXEC 命令被调用时执行。

*事务内部错误

事务可能会遇到两种类型的命令错误:

  • 在调用 EXEC 之前出错,命令加入队列失败。例如,命令的参数个数不符,或者内存补足。
  • 命令可能在执行 EXEC 之后失败,例如,对错误值类型的key执行操作(像在字符串值上执行列表操作)。

Clients used to sense the first kind of errors, happening before the EXEC call, by checking the return value of the queued command: if the command replies with QUEUED it was queued correctly, otherwise Redis returns an error. If there is an error while queueing a command, most clients will abort the transaction discarding it.

However starting with Redis 2.6.5, the server will remember that there was an error during the accumulation of commands, and will refuse to execute the transaction returning also an error during EXEC, and discarding the transaction automatically.

Before Redis 2.6.5 the behavior was to execute the transaction with just the subset of commands queued successfully in case the client called EXEC regardless of previous errors. The new behavior makes it much more simple to mix transactions with pipelining, so that the whole transaction can be sent at once, reading all the replies later at once.

Errors happening after EXEC instead are not handled in a special way: all the other commands will be executed even if some command fails during the transaction.

This is more clear on the protocol level. In the following example one command will fail when executed even if the syntax is right:

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 returned two-element Bulk string reply where one is an OK code and the other an -ERR reply. It's up to the client library to find a sensible way to provide the error to the user.

It's important to note that even when a command fails, all the other commands in the queue are processed -- Redis will not stop the processing of commands.

Another example, again using the wire protocol with telnet, shows how syntax errors are reported ASAP instead:

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

This time due to the syntax error the bad INCR command is not queued at all.

*为什么Redis不支持回滚?

如果你了解关系数据库,那么Redis事务处理机制看起来有点奇怪。

Redis事务中的命令允许失败,但是Redis会继续执行其它的命令而不是回滚所有命令。

这么做的原因有两点:

  • Redis 命令只在两种情况失败:
    • 语法错误的时候才失败(在命令输入的时候不检查语法)。
    • 要执行的key数据类型不匹配:这种错误实际上是编程错误,这应该在开发阶段被测试出来,而不是生产上。
  • 因为不需要回滚,所以Redis内部实现简单并高效。

当出现bug的时候Redis的这种做法并不友好,可是需要注意的是回滚并不能解决程序bug。

例如,对于需要增加1的逻辑增加了2,或者操作的key类型不对,这些情况回滚并没有什么帮助。

考虑到没有人能避免程序员错误,并且这种错误也基本不能进入生产环境,我们选择了更简单且更高效的方法,不支持错误回滚。

*丢弃命令队列

DISCARD 可用于中止事物。在这种情况下,不执行任何命令,连接状态恢复正常。

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

*乐观锁使用 check-and-set

WATCH 用于为 Redis 事务提供检查和设置 (CAS) 行为。

WATCHed keys are monitored in order to detect changes against them. If at least one watched key is modified before the EXEC command, the whole transaction aborts, and EXEC returns a Null reply to notify that the transaction failed.

For example, imagine we have the need to atomically increment the value of a key by 1 (let's suppose Redis doesn't have INCR).

The first try may be the following:

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

This will work reliably only if we have a single client performing the operation in a given time. If multiple clients try to increment the key at about the same time there will be a race condition. For instance, client A and B will read the old value, for instance, 10. The value will be incremented to 11 by both the clients, and finally SET as the value of the key. So the final value will be 11 instead of 12.

Thanks to WATCH we are able to model the problem very well:

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

Using the above code, if there are race conditions and another client modifies the result of val in the time between our call to WATCH and our call to EXEC, the transaction will fail.

We just have to repeat the operation hoping this time we'll not get a new race. This form of locking is called optimistic locking and is a very powerful form of locking. In many use cases, multiple clients will be accessing different keys, so collisions are unlikely -- usually there's no need to repeat the operation.

*WATCH 解释

WATCH 命令用于标记要监视的key,以便有条件地执行事务。只有当被 WATCH 的key 被修改时,事务才会被执行。

WATCH 可以被调用多次,从调用开始,直到EXEC调用结束,WATCH可以一次观察多个key。

一旦执行 EXEC 或者关闭客户端连接,所有的watch 都会被取消。

也可以使用不带参数的 UNWATCH 取消对所有 key 观察行为。有时这很有用,因为我们乐观地锁定了一些键,可能需要执行事务来修改这些键,但在读取键的内容后又不想继续修改,这时,我们只需调用 UNWATCH

*使用 WATCH 实现 ZPOP

使用 WATCH 来实现 Redis 自身不支持的命令ZPOP,并且这个操作是原子的。原子的从有序集中弹出分数较低的元素:

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

如果 EXEC 执行失败 (例如返回  Null reply) ,重新执行即可。

*Redis 脚本和事务

Redis script 支持事务, Redis 事务能做的事情,都可以用脚本来做,通常脚本会更简单、更快。