当客户端处于非事务状态下时, 所有发送给服务器端的命令都会立即被服务器执行。但是, 当客户端进入事务状态之后, 服务器在收到来自客户端的命令时, 不会立即执行命令, 而是将这些命令全部放进一个事务队列里, 然后返回 QUEUED , 表示命令已入队。在 PHP 中执行如下命令,其实是错误的,因为执行 exec 后才会统一作为数组返回!
$redis->watch($key); $redis->multi(); if ($redis->get($key) == $random) { $redis->del($key); } $redis->exec()
但其实并不是所有的命令都会被放进事务队列, 其中的例外就是 EXEC 、 DISCARD 、 MULTI 和 WATCH 这四个命令 —— 当这四个命令从客户端发送到服务器时, 它们会像客户端处于非事务状态一样, 直接被服务器执行。
事务的执行可以用如下伪代码描述:
def execute_transaction(): # 创建空白的回复队列 reply_queue = [] # 取出事务队列里的所有命令、参数和参数数量 for cmd, argv, argc in client.transaction_queue: # 执行命令,并取得命令的返回值 reply = execute_redis_command(cmd, argv, argc) # 将返回值追加到回复队列末尾 reply_queue.append(reply) # 清除客户端的事务状态 clear_transaction_state(client) # 清空事务队列 clear_transaction_queue(client) # 将事务的执行结果返回给客户端 send_reply_to_client(client, reply_queue)
不过事务中的命令和普通命令在执行上还是有一点区别的,其中最重要的两点是:
非事务状态下的命令以单个命令为单位执行,前一个命令和后一个命令的客户端不一定是同一个。而事务状态则是以一个事务为单位,执行 exec 后(放入队列不算)会执行事务队列中的所有命令,除非当前事务执行完毕,否则服务器不会中断事务,也不会执行其他客户端的其他命令。执行 exec 后才进入真正执行事务状态(不被中断)。
在非事务状态下,执行命令所得的结果会立即被返回给客户端,而事务则是将所有命令的结果集合到回复队列,再作为 EXEC 命令的结果返回给客户端。
DISCARD 命令用于取消一个事务, 它清空客户端的整个事务队列, 然后将客户端从事务状态调整回非事务状态, 最后返回字符串 OK 给客户端, 说明事务已被取消。
Redis 的事务是不可嵌套的, 当客户端已经处于事务状态, 而客户端又再向服务器发送 MULTI 时, 服务器只是简单地向客户端发送一个错误, 然后继续等待其他命令的入队。 MULTI 命令的发送不会造成整个事务失败, 也不会修改事务队列中已有的数据。
WATCH 只能在客户端进入事务状态之前执行, 在事务状态下发送 WATCH 命令会引发一个错误, 但它不会造成整个事务失败, 也不会修改事务队列中已有的数据(和前面处理 MULTI 的情况一样)。该命令用于在事务开始之前监视任意数量的键,当调用 EXEC 命令执行事务时, 如果任意一个被监视的键已经被其他客户端修改了, 那么整个事务不再执行, 直接返回失败。 即下图中客户端 A 将执行失败。
在时间 T4 ,客户端 B 修改了 name 键的值, 当客户端 A 在 T5 执行 EXEC 时,Redis 会发现 name 这个被监视的键已经被修改, 因此客户端 A 的事务不会被执行,而是直接返回失败。
WATCH 实现:在每个代表数据库的 redis.h/redisDb 结构类型中, 都保存了一个 watched_keys 字典, 字典的键是这个数据库被监视的键, 而字典的值则是一个链表, 链表中保存了所有监视这个键的客户端。
举个例子, 如果当前客户端为 client10086 , 那么当客户端执行 WATCH key1 key2 时, 前面展示的 watched_keys 将被修改成这个样子:
在任何对数据库键空间(key space)进行修改的命令成功执行之后 (比如 FLUSHDB 、 SET 、 DEL 、 LPUSH 、 SADD 、 ZREM ,诸如此类), multi.c/touchWatchedKey 函数都会被调用 —— 它检查数据库的 watched_keys 字典, 看是否有客户端在监视已经被命令修改的键, 如果有的话, 程序将所有监视这个/这些被修改键的客户端的 REDIS_DIRTY_CAS 选项打开。
当客户端发送 EXEC 命令、触发事务执行时, 服务器会对客户端的状态进行检查:
如果客户端的 REDIS_DIRTY_CAS 选项已经被打开,那么说明被客户端监视的键至少有一个已经被修改了,事务的安全性已经被破坏。服务器会放弃执行这个事务,直接向客户端返回空回复,表示事务执行失败。
如果 REDIS_DIRTY_CAS 选项没有被打开,那么说明所有监视键都安全,服务器正式执行事务。
#参考实现代码
def check_safety_before_execute_trasaction(): if client.state & REDIS_DIRTY_CAS: # 安全性已破坏,清除事务状态 clear_transaction_state(client) # 清空事务队列 clear_transaction_queue(client) # 返回空回复给客户端 send_empty_reply(client) else: # 安全性完好,执行事务 execute_transaction()
Redis 事务的 ACID 性质,Redis 只能保证一致性和隔离性(执行时不被别的客户端打扰)
原子性:
单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
如果一个事务队列中的所有命令都被成功地执行,那么称这个事务执行成功。
另一方面,如果 Redis 服务器进程在执行事务的过程中被停止 —— 比如接到 KILL 信号、宿主机器停机,等等,那么事务执行失败。
当事务失败时,Redis 也不会进行任何的重试或者回滚动作。
一致性:
Redis 的一致性问题可以分为三部分来讨论:入队错误、执行错误、Redis 进程被终结。
入队错误:在命令入队的过程中,如果客户端向服务器发送了错误的命令,比如命令的参数数量不对,等等, 那么服务器将向客户端返回一个出错信息, 并且将客户端的事务状态设为 REDIS_DIRTY_EXEC 。因此,带有不正确入队命令的事务不会被执行,也不会影响数据库的一致性。
redis 127.0.0.1:6379> MULTI OK redis 127.0.0.1:6379> set key (error) ERR wrong number of arguments for 'set' command redis 127.0.0.1:6379> EXISTS key QUEUED redis 127.0.0.1:6379> EXEC (error) EXECABORT Transaction discarded because of previous errors.
执行错误:如果命令在事务执行的过程中发生错误,比如说,对一个不同类型的 key 执行了错误的操作, 那么 Redis 只会将错误包含在事务的结果中, 这不会引起事务中断或整个失败,不会影响已执行事务命令的结果,也不会影响后面要执行的事务命令, 所以它对事务的一致性也没有影响。
隔离性:
Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,也不会执行其他客户端的命令,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。
持久性:
事务的持久性由 Redis 所使用的持久化模式决定。
参考:http://redisbook.readthedocs.io/en/latest/feature/transaction.html