Redis中的事务和监视语句

42

请解释一下"The Little Redis Book"中的以下示例:

通过上面的代码,我们无法实现自己的incr命令,因为它们在执行exec后一起执行。从代码上来看,我们不能这样做:

redis.multi() 
current = redis.get('powerlevel') 
redis.set('powerlevel', current + 1) 
redis.exec()

这不是 Redis 事务的工作方式。但是,如果我们在 powerlevel 上添加一个 watch,我们可以这样做:

redis.watch('powerlevel') 
current = redis.get('powerlevel') 
redis.multi() 
redis.set('powerlevel', current + 1) 
redis.exec()

如果我们调用watch方法后,另一个客户端更改了powerlevel的值,我们的事务将失败。如果没有客户端更改该值,则设置将起作用。我们可以在循环中执行此代码,直到它起作用。

为什么我们不能在事务中执行不会被其他命令中断的递增操作?为什么我们需要迭代并等待直到事务开始之前没有人更改值?


你知道 Redis 中的 incr 命令吗? 它可以在不使用事务的情况下,完美地实现你在示例中想要的功能。 当然,这并不是对问题本身的回答,但了解这一点仍然很有价值。 - polvoazul
3
@polvoazul,我知道这个命令,谢谢。这只是一个普通问题,并没有实际情况引起的。 - Marboni
1个回答

101

这里有几个问题。

1)为什么我们不能在不被其他命令中断的事务中执行增量操作?

请注意,Redis的“事务”与大多数人认为的传统DBMS中的事务完全不同。

# Does not work
redis.multi() 
current = redis.get('powerlevel') 
redis.set('powerlevel', current + 1) 
redis.exec()

您需要了解在服务器端(Redis)执行的内容,以及在客户端脚本中执行的内容。在上面的代码中,GET和SET命令将在Redis端执行,但是对当前值进行赋值和计算当前值加1应该在客户端上执行。

为了保证原子性,MULTI/EXEC块会延迟Redis命令的执行,直到执行exec。因此,客户端只会在内存中堆积GET和SET命令,并在最后一次性地、原子性地执行它们。当然,在尝试将当前值分配给GET的结果并进行增量操作之前就会发生。实际上,redis.get方法只会返回字符串“QUEUED”,以表示命令已被延迟,增量操作将不起作用。

在MULTI/EXEC块中,您只能使用那些参数在块开始之前就可以完全确定的命令。您可能需要阅读文档以获取更多信息。

2) 为什么我们需要迭代,等待没有人更改值,然后再开始事务?

这是一个并发乐观模式的例子。

如果我们不使用WATCH/MULTI/EXEC,我们将面临潜在的竞争条件:

# Initial arbitrary value
powerlevel = 10
session A: GET powerlevel -> 10
session B: GET powerlevel -> 10
session A: current = 10 + 1
session B: current = 10 + 1
session A: SET powerlevel 11
session B: SET powerlevel 11
# In the end we have 11 instead of 12 -> wrong

现在让我们添加一个 WATCH/MULTI/EXEC 块。使用 WATCH 子句,仅当值未更改时才执行 MULTI 和 EXEC 之间的命令。

# Initial arbitrary value
powerlevel = 10
session A: WATCH powerlevel
session B: WATCH powerlevel
session A: GET powerlevel -> 10
session B: GET powerlevel -> 10
session A: current = 10 + 1
session B: current = 10 + 1
session A: MULTI
session B: MULTI
session A: SET powerlevel 11 -> QUEUED
session B: SET powerlevel 11 -> QUEUED
session A: EXEC -> success! powerlevel is now 11
session B: EXEC -> failure, because powerlevel has changed and was watched
# In the end, we have 11, and session B knows it has to attempt the transaction again
# Hopefully, it will work fine this time.

所以您不需要迭代等待,直到没有人更改值,而是一遍又一遍地尝试操作,直到Redis确信值是一致的并发出成功信号。
在大多数情况下,如果“事务”足够快且争用的概率很低,则更新非常高效。现在,如果存在争用,则必须为某些“事务”执行一些额外的操作(由于迭代和重试)。但数据始终是一致的,并且不需要锁定。

那么,这里有什么“公平性”吗?如果我进行了“观察”,但失败了怎么办?那么我就必须重试。如果存在争用 - 我能够无限次尝试而不“获取锁”吗? - Brad
所以我从你的例子中理解,如果你展示了一个使用MULTI/EXEC但没有WATCH的例子,两个会话都可能读取值10,并且Redis会写回11。这将使写回“原子化”,但不能防止客户端在本地读取和增加相同的值,导致错误的答案? - Brad
1
没有公平保证。然而,在实践中,客户端在watch/multi/exec执行时出现饥饿的情况并不常见。 - Didier Spezia
第二个问题:是的,完全正确。 - Didier Spezia
你必须再次检查 powerlevel,因为你无法保证第三方会同时干扰 powerlevel。 - Didier Spezia
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接