redis事务

事务

Redis 经过 MULTI 、 DISCARD 、 EXEC 和 WATCH 四个命令来实现事务功能, 本章首先讨论使用 MULTI 、 DISCARD 和 EXEC 三个命令实现的通常事务, 而后再来讨论带有 WATCH 的事务的实现。html

由于事务的安全性也很是重要, 因此本章最后经过常见的 ACID 性质对 Redis 事务的安全性进行了说明。node

事务

事务提供了一种“将多个命令打包, 而后一次性、按顺序地执行”的机制, 而且事务在执行的期间不会主动中断 —— 服务器在执行完事务中的全部命令以后, 才会继续处理其余客户端的其余命令。redis

如下是一个事务的例子, 它先以 MULTI 开始一个事务, 而后将多个命令入队到事务中, 最后由 EXEC 命令触发事务, 一并执行事务中的全部命令:数据库

redis> MULTI
OK

redis> SET book-name "Mastering C++ in 21 days"
QUEUED

redis> GET book-name
QUEUED

redis> SADD tag "C++" "Programming" "Mastering Series"
QUEUED

redis> SMEMBERS tag
QUEUED

redis> EXEC
1) OK
2) "Mastering C++ in 21 days"
3) (integer) 3
4) 1) "Mastering Series"
   2) "C++"
   3) "Programming"

一个事务从开始到执行会经历如下三个阶段:数组

  1. 开始事务。
  2. 命令入队。
  3. 执行事务。

下文将分别介绍事务的这三个阶段。安全

开始事务

MULTI 命令的执行标记着事务的开始:服务器

redis> MULTI
OK

这个命令惟一作的就是, 将客户端的 REDIS_MULTI 选项打开, 让客户端从非事务状态切换到事务状态。app

digraph normal_to_transaction {      rankdir = LR;      node [shape = circle, style = filled];      edge [style = bold];      label = "客户端状态的切换";      normal [label = "非事务状态", fillcolor = "#FADCAD"];      transaction [label = "事务状态", fillcolor = "#A8E270"];      normal -> transaction [label = "打开选项\nREDIS_MULTI"]; }

命令入队

当客户端处于非事务状态下时, 全部发送给服务器端的命令都会当即被服务器执行:async

redis> SET msg "hello moto"
OK

redis> GET msg
"hello moto"

可是, 当客户端进入事务状态以后, 服务器在收到来自客户端的命令时, 不会当即执行命令, 而是将这些命令所有放进一个事务队列里, 而后返回 QUEUED , 表示命令已入队:函数

redis> MULTI
OK

redis> SET msg "hello moto"
QUEUED

redis> GET msg
QUEUED

如下流程图展现了这一行为:

digraph enqueue {      node [shape = plaintext, style = filled];      edge [style = bold];      command_in [label = "服务器接到来自客户端的命令"];      in_transaction_or_not [label = "客户端是否正处于事务状态?", shape = diamond, fillcolor = "#95BBE3"];      enqueu_command [label = "将命令放进事务队列里", fillcolor = "#A8E270"];      return_enqueued [label = "向客户端返回 QUEUED 字符串\n表示命令已入队", fillcolor = "#A8E270"];      exec_command [label = "执行命令", fillcolor = "#FADCAD"];      return_command_result [label = "向客户端返回命令的执行结果", fillcolor = "#FADCAD"];      //       command_in -> in_transaction_or_not;      in_transaction_or_not -> enqueu_command [label = "是"];      in_transaction_or_not -> exec_command [label = "否"];      exec_command -> return_command_result;      enqueu_command -> return_enqueued; }

事务队列是一个数组, 每一个数组项是都包含三个属性:

  1. 要执行的命令(cmd)。
  2. 命令的参数(argv)。
  3. 参数的个数(argc)。

举个例子, 若是客户端执行如下命令:

redis> MULTI
OK

redis> SET book-name "Mastering C++ in 21 days"
QUEUED

redis> GET book-name
QUEUED

redis> SADD tag "C++" "Programming" "Mastering Series"
QUEUED

redis> SMEMBERS tag
QUEUED

那么程序将为客户端建立如下事务队列:

数组索引 cmd argv argc
0 SET ["book-name", "Mastering C++ in 21 days"] 2
1 GET ["book-name"] 1
2 SADD ["tag", "C++", "Programming", "Mastering Series"] 4
3 SMEMBERS ["tag"] 1

执行事务

前面说到, 当客户端进入事务状态以后, 客户端发送的命令就会被放进事务队列里。

但其实并非全部的命令都会被放进事务队列, 其中的例外就是 EXEC 、 DISCARD 、 MULTI 和 WATCH 这四个命令 —— 当这四个命令从客户端发送到服务器时, 它们会像客户端处于非事务状态同样, 直接被服务器执行:

digraph not_enque_command {      node [shape = plaintext, style = filled];      edge [style = bold];      command_in [label = "服务器接到来自客户端的命令"];      in_transaction_or_not [label = "客户端是否正处于事务状态?", shape = diamond, fillcolor = "#95BBE3"];      not_exec_and_discard [label = "命令是否\nEXEC 、 DISCARD 、\nMULTI 或 WATCH ?", shape = diamond, fillcolor = "#FFC1C1"];      enqueu_command [label = "将命令放进事务队列里", fillcolor = "#A8E270"];      return_enqueued [label = "向客户端返回 QUEUED 字符串\n表示命令已入队", fillcolor = "#A8E270"];      exec_command [label = "执行命令", fillcolor = "#FADCAD"];      return_command_result [label = "向客户端返回命令的执行结果", fillcolor = "#FADCAD"];      //       command_in -> in_transaction_or_not;      in_transaction_or_not -> not_exec_and_discard [label = "是"];      not_exec_and_discard -> enqueu_command [label = "否"];      not_exec_and_discard -> exec_command [label = "是"];      in_transaction_or_not -> exec_command [label = "否"];      exec_command -> return_command_result;      enqueu_command -> return_enqueued; }

若是客户端正处于事务状态, 那么当 EXEC 命令执行时, 服务器根据客户端所保存的事务队列, 以先进先出(FIFO)的方式执行事务队列中的命令: 最早入队的命令最早执行, 而最后入队的命令最后执行。

好比说,对于如下事务队列:

数组索引 cmd argv argc
0 SET ["book-name", "Mastering C++ in 21 days"] 2
1 GET ["book-name"] 1
2 SADD ["tag", "C++", "Programming", "Mastering Series"] 4
3 SMEMBERS ["tag"] 1

程序会首先执行 SET 命令, 而后执行 GET 命令, 再而后执行 SADD 命令, 最后执行 SMEMBERS 命令。

执行事务中的命令所得的结果会以 FIFO 的顺序保存到一个回复队列中。

好比说,对于上面给出的事务队列,程序将为队列中的命令建立以下回复队列:

数组索引 回复类型 回复内容
0 status code reply OK
1 bulk reply "Mastering C++ in 21 days"
2 integer reply 3
3 multi-bulk reply ["Mastering Series", "C++", "Programming"]

当事务队列里的全部命令被执行完以后, EXEC 命令会将回复队列做为本身的执行结果返回给客户端, 客户端从事务状态返回到非事务状态, 至此, 事务执行完毕。

事务的整个执行过程能够用如下伪代码表示:

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)

在事务和非事务状态下执行命令

不管在事务状态下, 仍是在非事务状态下, Redis 命令都由同一个函数执行, 因此它们共享不少服务器的通常设置, 好比 AOF 的配置、RDB 的配置,以及内存限制,等等。

不过事务中的命令和普通命令在执行上仍是有一点区别的,其中最重要的两点是:

  1. 非事务状态下的命令以单个命令为单位执行,前一个命令和后一个命令的客户端不必定是同一个;

    而事务状态则是以一个事务为单位,执行事务队列中的全部命令:除非当前事务执行完毕,不然服务器不会中断事务,也不会执行其余客户端的其余命令。

  2. 在非事务状态下,执行命令所得的结果会当即被返回给客户端;

    而事务则是将全部命令的结果集合到回复队列,再做为 EXEC 命令的结果返回给客户端。

事务状态下的 DISCARD 、 MULTI 和 WATCH 命令

除了 EXEC 以外, 服务器在客户端处于事务状态时, 不加入到事务队列而直接执行的另外三个命令是 DISCARD 、 MULTI 和 WATCH 。

DISCARD 命令用于取消一个事务, 它清空客户端的整个事务队列, 而后将客户端从事务状态调整回非事务状态, 最后返回字符串 OK 给客户端, 说明事务已被取消。

Redis 的事务是不可嵌套的, 当客户端已经处于事务状态, 而客户端又再向服务器发送 MULTI 时, 服务器只是简单地向客户端发送一个错误, 而后继续等待其余命令的入队。 MULTI 命令的发送不会形成整个事务失败, 也不会修改事务队列中已有的数据。

WATCH 只能在客户端进入事务状态以前执行, 在事务状态下发送 WATCH 命令会引起一个错误, 但它不会形成整个事务失败, 也不会修改事务队列中已有的数据(和前面处理 MULTI 的状况同样)。

带 WATCH 的事务

WATCH 命令用于在事务开始以前监视任意数量的键: 当调用 EXEC 命令执行事务时, 若是任意一个被监视的键已经被其余客户端修改了, 那么整个事务再也不执行, 直接返回失败。

如下示例展现了一个执行失败的事务例子:

redis> WATCH name
OK

redis> MULTI
OK

redis> SET name peter
QUEUED

redis> EXEC
(nil)

如下执行序列展现了上面的例子是如何失败的:

时间 客户端 A 客户端 B
T1 WATCH name  
T2 MULTI  
T3 SET name peter  
T4   SET name john
T5 EXEC  

在时间 T4 ,客户端 B 修改了 name 键的值, 当客户端 A 在 T5 执行 EXEC 时,Redis 会发现 name 这个被监视的键已经被修改, 所以客户端 A 的事务不会被执行,而是直接返回失败。

下文就来介绍 WATCH 的实现机制,而且看看事务系统是如何检查某个被监视的键是否被修改,从而保证事务的安全性的。

WATCH 命令的实现

在每一个表明数据库的 redis.h/redisDb 结构类型中, 都保存了一个 watched_keys 字典, 字典的键是这个数据库被监视的键, 而字典的值则是一个链表, 链表中保存了全部监视这个键的客户端。

好比说,如下字典就展现了一个 watched_keys 字典的例子:

digraph watched_keys {      rankdir = LR;      node [shape = record, style = filled];      edge [style = bold];      // keys      watched_keys [label = "watched_keys |<key1> key1 |<key2> key2 |<key3> key3 | ... |<keyN> keyN", fillcolor = "#A8E270"];      // clients blocking for key1     client1 [label = "client1", fillcolor = "#95BBE3"];     client5 [label = "client5", fillcolor = "#95BBE3"];     client2 [label = "client2", fillcolor = "#95BBE3"];     null_1 [label = "NULL", shape = plaintext];          watched_keys:key1 -> client2;     client2 -> client5;     client5 -> client1;     client1 -> null_1;      // clients blocking for key2     client7 [label = "client7", fillcolor = "#95BBE3"];     null_2 [label = "NULL", shape = plaintext];      watched_keys:key2 -> client7;     client7 -> null_2;      // key3      client3 [label = "client3", fillcolor = "#95BBE3"];     client4 [label = "client4", fillcolor = "#95BBE3"];     client6 [label = "client6", fillcolor = "#95BBE3"];     null_3 [label = "NULL", shape = plaintext];      watched_keys:key3 -> client3;     client3 -> client4;     client4 -> client6;     client6 -> null_3; }

其中, 键 key1 正在被 client2 、 client5 和 client1 三个客户端监视, 其余一些键也分别被其余别的客户端监视着。

WATCH 命令的做用, 就是将当前客户端和要监视的键在 watched_keys 中进行关联。

举个例子, 若是当前客户端为 client10086 , 那么当客户端执行 WATCH key1 key2 时, 前面展现的 watched_keys 将被修改为这个样子:

digraph new_watched_keys {      rankdir = LR;      node [shape = record, style = filled];      edge [style = bold];      // keys      watched_keys [label = "watched_keys |<key1> key1 |<key2> key2 |<key3> key3 | ... |<keyN> keyN", fillcolor = "#A8E270"];      // clients blocking for key1     client1 [label = "client1", fillcolor = "#95BBE3"];     client5 [label = "client5", fillcolor = "#95BBE3"];     client2 [label = "client2", fillcolor = "#95BBE3"];     client10086 [label = "client10086", fillcolor = "#FFC1C1"];     null_1 [label = "NULL", shape = plaintext];          watched_keys:key1 -> client2;     client2 -> client5;     client5 -> client1;     client1 -> client10086;     client10086 -> null_1;      // clients blocking for key2     client7 [label = "client7", fillcolor = "#95BBE3"];     client10086_2 [label = "client10086", fillcolor = "#FFC1C1"];     null_2 [label = "NULL", shape = plaintext];      watched_keys:key2 -> client7;     client7 -> client10086_2;     client10086_2 -> null_2;      // key3      client3 [label = "client3", fillcolor = "#95BBE3"];     client4 [label = "client4", fillcolor = "#95BBE3"];     client6 [label = "client6", fillcolor = "#95BBE3"];     null_3 [label = "NULL", shape = plaintext];      watched_keys:key3 -> client3;     client3 -> client4;     client4 -> client6;     client6 -> null_3; }

经过 watched_keys 字典, 若是程序想检查某个键是否被监视, 那么它只要检查字典中是否存在这个键便可; 若是程序要获取监视某个键的全部客户端, 那么只要取出键的值(一个链表), 而后对链表进行遍历便可。

WATCH 的触发

在任何对数据库键空间(key space)进行修改的命令成功执行以后 (好比 FLUSHDB 、 SET 、 DEL 、 LPUSH 、 SADD 、 ZREM ,诸如此类), multi.c/touchWatchedKey 函数都会被调用 —— 它检查数据库的 watched_keys 字典, 看是否有客户端在监视已经被命令修改的键, 若是有的话, 程序将全部监视这个/这些被修改键的客户端的 REDIS_DIRTY_CAS 选项打开:

digraph dirty_cas {      rankdir = LR;      node [shape = circle, style = filled];      edge [style = bold];      label = "客户端状态的切换";      normal [label = "非事务状态", fillcolor = "#FADCAD"];      transaction [label = "事务状态", fillcolor = "#A8E270"];      dirty_cas [label = "事务安全性\n已被破坏", fillcolor = "#B22222"];      normal -> transaction [label = "打开选项\nREDIS_MULTI"];      transaction -> dirty_cas [label = "打开选项\nREDIS_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()

举个例子,假设数据库的 watched_keys 字典以下图所示:

digraph watched_keys {      rankdir = LR;      node [shape = record, style = filled];      edge [style = bold];      // keys      watched_keys [label = "watched_keys |<key1> key1 |<key2> key2 |<key3> key3 | ... |<keyN> keyN", fillcolor = "#A8E270"];      // clients blocking for key1     client1 [label = "client1", fillcolor = "#95BBE3"];     client5 [label = "client5", fillcolor = "#95BBE3"];     client2 [label = "client2", fillcolor = "#95BBE3"];     null_1 [label = "NULL", shape = plaintext];          watched_keys:key1 -> client2;     client2 -> client5;     client5 -> client1;     client1 -> null_1;      // clients blocking for key2     client7 [label = "client7", fillcolor = "#95BBE3"];     null_2 [label = "NULL", shape = plaintext];      watched_keys:key2 -> client7;     client7 -> null_2;      // key3      client3 [label = "client3", fillcolor = "#95BBE3"];     client4 [label = "client4", fillcolor = "#95BBE3"];     client6 [label = "client6", fillcolor = "#95BBE3"];     null_3 [label = "NULL", shape = plaintext];      watched_keys:key3 -> client3;     client3 -> client4;     client4 -> client6;     client6 -> null_3; }

若是某个客户端对 key1 进行了修改(好比执行 DEL key1 ), 那么全部监视 key1 的客户端, 包括 client2 、 client5 和 client1 的 REDIS_DIRTY_CAS 选项都会被打开, 当客户端 client2 、 client5 和 client1 执行 EXEC 的时候, 它们的事务都会以失败了结。

最后,当一个客户端结束它的事务时,不管事务是成功执行,仍是失败, watched_keys 字典中和这个客户端相关的资料都会被清除。

事务的 ACID 性质

勘误:Redis 的事务是保证原子性的,本节的内容将原子性和回滚功能混淆了,等待修复中。 —— 2013.6.23

在传统的关系式数据库中,经常用 ACID 性质来检验事务功能的安全性。

Redis 事务保证了其中的一致性(C)和隔离性(I),但并不保证原子性(A)和持久性(D)。

如下四小节是关于这四个性质的详细讨论。

原子性(Atomicity)

单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增长任何维持原子性的机制,因此 Redis 事务的执行并非原子性的。

若是一个事务队列中的全部命令都被成功地执行,那么称这个事务执行成功。

另外一方面,若是 Redis 服务器进程在执行事务的过程当中被中止 —— 好比接到 KILL 信号、宿主机器停机,等等,那么事务执行失败。

当事务失败时,Redis 也不会进行任何的重试或者回滚动做。

一致性(Consistency)

Redis 的一致性问题能够分为三部分来讨论:入队错误、执行错误、Redis 进程被终结。

入队错误

在命令入队的过程当中,若是客户端向服务器发送了错误的命令,好比命令的参数数量不对,等等, 那么服务器将向客户端返回一个出错信息, 而且将客户端的事务状态设为 REDIS_DIRTY_EXEC 。

当客户端执行 EXEC 命令时, 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 所使用的持久化模式,可能有如下状况出现:

  • 内存模式:若是 Redis 没有采起任何持久化机制,那么重启以后的数据库老是空白的,因此数据老是一致的。

  • RDB 模式:在执行事务时,Redis 不会中断事务去执行保存 RDB 的工做,只有在事务执行以后,保存 RDB 的工做才有可能开始。因此当 RDB 模式下的 Redis 服务器进程在事务中途被杀死时,事务内执行的命令,无论成功了多少,都不会被保存到 RDB 文件里。恢复数据库须要使用现有的 RDB 文件,而这个 RDB 文件的数据保存的是最近一次的数据库快照(snapshot),因此它的数据可能不是最新的,但只要 RDB 文件自己没有由于其余问题而出错,那么还原后的数据库就是一致的。

  • AOF 模式:由于保存 AOF 文件的工做在后台线程进行,因此即便是在事务执行的中途,保存 AOF 文件的工做也能够继续进行,所以,根据事务语句是否被写入并保存到 AOF 文件,有如下两种状况发生:

    1)若是事务语句未写入到 AOF 文件,或 AOF 未被 SYNC 调用保存到磁盘,那么当进程被杀死以后,Redis 能够根据最近一次成功保存到磁盘的 AOF 文件来还原数据库,只要 AOF 文件自己没有由于其余问题而出错,那么还原后的数据库老是一致的,但其中的数据不必定是最新的。

    2)若是事务的部分语句被写入到 AOF 文件,而且 AOF 文件被成功保存,那么不完整的事务执行信息就会遗留在 AOF 文件里,当重启 Redis 时,程序会检测到 AOF 文件并不完整,Redis 会退出,并报告错误。须要使用 redis-check-aof 工具将部分红功的事务命令移除以后,才能再次启动服务器。还原以后的数据老是一致的,并且数据也是最新的(直到事务执行以前为止)。

隔离性(Isolation)

Redis 是单进程程序,而且它保证在执行事务时,不会对事务进行中断,事务能够运行直到执行完全部事务队列中的命令为止。所以,Redis 的事务是老是带有隔离性的。

持久性(Durability)

由于事务不过是用队列包裹起了一组 Redis 命令,并无提供任何额外的持久性功能,因此事务的持久性由 Redis 所使用的持久化模式决定:

  • 在单纯的内存模式下,事务确定是不持久的。

  • 在 RDB 模式下,服务器可能在事务执行以后、RDB 文件更新以前的这段时间失败,因此 RDB 模式下的 Redis 事务也是不持久的。

  • 在 AOF 的“老是 SYNC ”模式下,事务的每条命令在执行成功以后,都会当即调用 fsync 或 fdatasync 将事务数据写入到 AOF 文件。可是,这种保存是由后台线程进行的,主线程不会阻塞直到保存成功,因此从命令执行成功到数据保存到硬盘之间,仍是有一段很是小的间隔,因此这种模式下的事务也是不持久的。

    其余 AOF 模式也和“老是 SYNC ”模式相似,因此它们都是不持久的。

小结

  • 事务提供了一种将多个命令打包,而后一次性、有序地执行的机制。
  • 事务在执行过程当中不会被中断,全部事务命令执行完以后,事务才能结束。
  • 多个命令会被入队到事务队列中,而后按先进先出(FIFO)的顺序执行。
  • 带 WATCH 命令的事务会将客户端和被监视的键在数据库的 watched_keys 字典中进行关联,当键被修改时,程序会将全部监视被修改键的客户端的 REDIS_DIRTY_CAS 选项打开。
  • 只有在客户端的 REDIS_DIRTY_CAS 选项未被打开时,才能执行事务,不然事务直接返回失败。
  • Redis 的事务保证了 ACID 中的一致性(C)和隔离性(I),但并不保证原子性(A)和持久性(D)。