有时我们需要多次调用 Redis,以便同时操作多个结构。 虽然有一些命令可以在键之间复制或移动项目,但没有单个命令可以在类型之间移动项目(尽管您可以使用 ZUNIONSTORE 从 SET 复制到 ZSET)。 对于涉及多个键(相同或不同类型)的操作,Redis 提供了五个命令来帮助我们在不中断的情况下操作多个键:WATCH、MULTI、EXEC、UNWATCH 和 DISCARD。
现在,我们只讨论 Redis 事务的最简单版本,它使用 MULTI 和 EXEC。 如果您想看一个使用 WATCH、MULTI、EXEC 和 UNWATCH 的例子,您可以跳到第 4.4 节,在那里我将解释为什么您需要将 WATCH 和 UNWATCH 与 MULTI 和 EXEC 一起使用。
在 Redis 中,涉及 MULTI 和 EXEC 的基本事务旨在为一个客户端提供执行多个命令 A、B、C... 的机会,而不会被其他客户端中断。 这与关系数据库事务不同,后者可以部分执行,然后回滚或提交。 在 Redis 中,作为基本 MULTI/EXEC 事务的一部分传递的每个命令都会一个接一个地执行,直到它们完成。 完成后,其他客户端可以执行他们的命令。
要在 Redis 中执行事务,我们首先调用 MULTI,然后是我们打算执行的任何命令序列,然后是 EXEC。 当看到 MULTI 时,Redis 会将来自同一连接的命令排队,直到看到 EXEC,此时 Redis 将按顺序无中断地执行排队的命令。 从语义上讲,我们的 Python 库通过使用所谓的*管道*来处理这个问题。 在连接对象上调用 pipeline() 方法将创建一个事务,正确使用时,它会自动将一系列命令封装在 MULTI 和 EXEC 中。 顺便说一句,Python Redis 客户端也会存储要发送的命令,直到我们真正想发送它们为止。 这减少了 Redis 和客户端之间的往返次数,可以提高一系列命令的性能。
就像 PUBLISH 和 SUBSCRIBE 一样,演示使用事务的结果的最简单方法是通过使用线程。 在下一个列表中,您可以看到没有事务的并行递增操作的结果。
>>> def notrans():
… print conn.incr('notrans:')
递增 ‘notrans:’ 计数器并打印结果。
… time.sleep(.1)
等待 100 毫秒。
… conn.incr('notrans:', -1)
递减 ‘notrans:’ 计数器。
… >>> if 1:
… for i in xrange(3): … threading.Thread(target=notrans).start()
启动三个线程来执行非事务性递增/睡眠/递减。
… time.sleep(.5)
等待半秒钟让一切完成。
…
1 2 3
因为没有事务,所以每个线程命令都可以自由交错,在这种情况下导致计数器稳定增长。
如果没有事务,三个线程中的每一个都能够在递减完成之前递增 notrans: 计数器。 通过包含 100 毫秒的睡眠时间,我们夸大了潜在的问题,但如果我们需要能够在没有其他命令干扰的情况下执行这两个调用,我们就会遇到问题。 以下列表显示了带有事务的相同操作。
>>> def trans():
… pipeline = conn.pipeline()
创建一个事务性管道。
… pipeline.incr('trans:')
将 ‘trans:’ 计数器递增排队。
… time.sleep(.1)
等待 100 毫秒。
… pipeline.incr('trans:', -1)
将 ‘trans:’ 计数器递减排队。
… print pipeline.execute()[0]
执行两个命令并打印递增操作的结果。
… >>> if 1:
… for i in xrange(3): … threading.Thread(target=trans).start()
启动三个事务性递增/睡眠/递减调用。
… time.sleep(.5)
等待半秒钟让一切完成。
…
1 1 1
因为每个递增/睡眠/递减对都在事务内部执行,所以没有其他命令可以交错,这使我们所有结果都为 1。
正如您所看到的,通过使用事务,每个线程都能够在没有其他线程中断的情况下执行其整个命令序列,尽管两个调用之间存在延迟。 同样,这是因为 Redis 等待执行 MULTI 和 EXEC 之间的所有提供的命令,直到收到所有命令并跟随 EXEC 为止。
使用事务既有好处也有缺点,我们将在第 4.4 节中进一步讨论。
MULTI/EXEC 事务的主要目的之一是消除所谓的 竞争条件,您在清单 3.13 中看到了它。 事实证明,第 1 章中的 article_vote() 函数存在竞争条件和第二个相关错误。 竞争条件可能导致内存泄漏,而该错误可能导致投票未正确计数。 发生这些情况的几率非常小,但是您能发现并修复它们吗? 提示:如果您在查找内存泄漏时遇到困难,请在查阅 post_article() 函数时查看第 6.2.5 节。
在 Redis 中使用管道的第二个目的是提高性能(我们将在第 4.4-4.6 节中对此进行更多讨论)。 特别是,通过减少在 Redis 和我们的客户端之间通过一系列命令发生的往返次数,我们可以显着减少客户端等待响应的时间。 在我们在第 1 章中定义的 get_articles() 函数中,实际上会有 26 次 Redis 和客户端之间的往返来获取一整页文章。 这是浪费。 您可以更改 get_articles() 以使其仅进行两次往返吗?
将数据写入 Redis 时,有时数据仅在短时间内有用。 我们可以在该时间过后手动删除此数据,或者我们可以让 Redis 通过使用键过期自动删除数据本身。