dot Redis 8 来了——而且是开源的

了解更多

3.7.2 基本 Redis 事务

返回主页

3.7.2 基本 Redis 事务

有时我们需要多次调用 Redis,以便同时操作多个结构。 虽然有一些命令可以在键之间复制或移动项目,但没有单个命令可以在类型之间移动项目(尽管您可以使用 ZUNIONSTORESET 复制到 ZSET)。 对于涉及多个键(相同或不同类型)的操作,Redis 提供了五个命令来帮助我们在不中断的情况下操作多个键:WATCHMULTIEXECUNWATCHDISCARD

现在,我们只讨论 Redis 事务的最简单版本,它使用 MULTIEXEC。 如果您想看一个使用 WATCHMULTIEXECUNWATCH 的例子,您可以跳到第 4.4 节,在那里我将解释为什么您需要将 WATCHUNWATCHMULTIEXEC 一起使用。

redis 中的基本事务是什么?

在 Redis 中,涉及 MULTIEXEC 的基本事务旨在为一个客户端提供执行多个命令 A、B、C... 的机会,而不会被其他客户端中断。 这与关系数据库事务不同,后者可以部分执行,然后回滚或提交。 在 Redis 中,作为基本 MULTI/EXEC 事务的一部分传递的每个命令都会一个接一个地执行,直到它们完成。 完成后,其他客户端可以执行他们的命令。

要在 Redis 中执行事务,我们首先调用 MULTI,然后是我们打算执行的任何命令序列,然后是 EXEC。 当看到 MULTI 时,Redis 会将来自同一连接的命令排队,直到看到 EXEC,此时 Redis 将按顺序无中断地执行排队的命令。 从语义上讲,我们的 Python 库通过使用所谓的*管道*来处理这个问题。 在连接对象上调用 pipeline() 方法将创建一个事务,正确使用时,它会自动将一系列命令封装在 MULTIEXEC 中。 顺便说一句,Python Redis 客户端也会存储要发送的命令,直到我们真正想发送它们为止。 这减少了 Redis 和客户端之间的往返次数,可以提高一系列命令的性能。

就像 PUBLISHSUBSCRIBE 一样,演示使用事务的结果的最简单方法是通过使用线程。 在下一个列表中,您可以看到没有事务的并行递增操作的结果。

清单 3.13 并行执行期间没有事务会发生什么
>>> 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 毫秒的睡眠时间,我们夸大了潜在的问题,但如果我们需要能够在没有其他命令干扰的情况下执行这两个调用,我们就会遇到问题。 以下列表显示了带有事务的相同操作。

清单 3.14 并行执行期间带有事务会发生什么
>>> 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 等待执行 MULTIEXEC 之间的所有提供的命令,直到收到所有命令并跟随 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 通过使用键过期自动删除数据本身。