乐观锁定不起作用怎么办?


11

我有以下这种情况:

  1. 用户向GET请求/projects/1并接收ETag
  2. 用户从步骤1 开始使用ETag 进行PUT请求/projects/1
  3. 用户/projects/1从步骤1 开始使用ETag 发出另一个PUT请求。

通常,第二个PUT请求将收到412响应,因为ETag现在已过时-第一个PUT请求修改了资源,因此ETag不再匹配。

但是,如果同时发送两个PUT请求(或者一个又一个发送)怎么办?在PUT#2到达之前,第一个PUT请求没有时间处理和更新资源,这导致PUT#2覆盖PUT#1。乐观锁定的全部目的是要避免这种情况的发生。


3
如Esben所述,将您在业务级别交易中的操作原子化。
罗伯特·哈维

如果我使用交易将操作原子化,会发生什么?在完全处理PUT#1之前,将不会处理PUT#2?
maximedupre

7
成为悲观主义者?
jpmc26 '18 -10-5

好吧,这就是锁定的目的。
Fattie

正确,当然不应处理Put#2-它们应该是唯一的。
Fattie

Answers:


21

ETag机制仅指定用于乐观锁定的通信协议。应用程序服务的责任是实现一种机制,以检测并发更新以强制执行乐观锁。

在使用数据库的典型应用程序中,通常可以通过在处理PUT请求时打开事务来做到这一点。通常,您需要读取该事务中数据库的现有状态(以获取读取锁定),检查Etag的有效性,并覆盖数据(以这种方式在发生任何不兼容的并发事务时会引起写冲突),然后提交。如果正确设置了事务,则提交之一将失败,因为它们都将尝试同时更新同一数据。如果对应用程序有意义,那么您就可以使用此事务失败返回412或重试该请求。


服务器当前实现检测并发更新的机制的方式是通过比较资源的哈希值。服务器还将事务用于所有操作,但是我没有获得任何锁,这可能是导致问题的原因。但是,在您的示例中,如果事务使用锁,提交中的一个怎么会出错?读取状态时,第二个事务应该挂起,直到第一个事务解决为止。
maximedupre

1
@maximedupre:如果您正在使用事务,则您拥有某种锁,尽管它可能是隐式锁(当您读取/更新字段而不是显式请求时,这些锁是自动获取的)。我上面描述的机制可以仅使用那些隐式锁定来实现。另一个问题是,它取决于您使用的数据库,但是许多现代数据库都使用MVCC(多版本并发控制)来允许多个读取器和写入器在相同的字段上工作,而不会不必要地互相阻塞。
Lie Ryan

1
警告:在许多DBMS(PostgreSQL,Oracle,SQL Server等)中,默认事务隔离级别为“读取已提交”,在这种情况下,您的方法不足以防止OP出现竞争状况。在此类DMBS中,您可以通过将其包含AND ETag = ...UPDATE语句的WHERE子句中,然后再检查更新的行数来对其进行修复。(或者通过使用更严格的事务隔离级别,但我不建议这样做。)
ruakh

1
@ruakh:这取决于您编写查询的方式,是的,默认隔离级别不会自动为所有查询提供这种行为,但是通常可以采用足以实现乐观锁定的方式来构造事务。在大多数情况下,如果事务一致性在应用程序中很重要,那么无论如何我还是建议将可重复读取作为默认隔离级别。在使用MVCC的数据库中,可重复读取的开销非常小,并且极大地简化了应用程序。
Lie Ryan

1
@ruakh:可重复读取的主要缺点是,如果存在并发事务,则必须准备重试或失败。这通常是一个问题,但是提供乐观锁定作为并发策略的应用程序已经需要进行此处理,因此可重复的读取失败自然会映射为乐观锁定失败,并且实际上不会增加​​新的缺点。
Lie Ryan

13

您必须自动执行以下对:

  • 检查标签的有效性(即是最新的)
  • 更新资源(包括更新其标签)

其他人则将其称为事务处理-但从根本上讲,这两个操作的原子执行是防止因偶然的时机而覆盖另一个操作的原因。值得注意的是,如果没有此选项,您将面临比赛条件。

如果您查看大图,这仍然被认为是乐观锁定:资源本身不受任何用户或正在查看数据的任何用户的初始读取(GET)锁定,无论是否要进行更新。

某些原子行为是必需的,但这是在单个请求(PUT)中发生的,而不是尝试对多个网络交互进行锁定。这是乐观锁定:对象未被GET锁定,但仍可以由PUT安全地更新。

还有两种方法可以实现这两个操作的原子执行-锁定资源不是唯一的选择;例如,轻量级线程或对象锁可能就足够了,并且取决于应用程序的体系结构和执行上下文。


4
+1表示重要的是原子。取决于要更新的​​基础资源,这可以在没有事务或锁定的情况下完成。例如,内存中资源的原子比较和交换,或持久化数据的事件源。
艾伦·艾希巴赫

@ AaronM.Eshbach,表示同意,并感谢您提出要求。
Erik Eidt '18

1

由应用程序开发人员实际检查E-Tag并提供该逻辑。Web服务器为您完成任务并不是魔术,因为它只知道如何计算E-Tag静态内容的标头。因此,让我们将您的情况放在上面,并分解交互应该如何发生。

GET /projects/1

服务器接收到请求,确定该记录版本的E-Tag,并将其与实际内容一起返回。

200 - OK
E-Tag: "412"
Content-Type: application/json
{modified: false}

由于客户端现在具有E-Tag值,因此可以将其包含在PUT请求中:

PUT /projects/1
If-Match: "412"
Content-Type: application/json
{modified: true}

此时,您的应用程序必须执行以下操作:

  • 验证E-Tag是否仍然正确:“ 412” ==“ 412”?
  • 如果是这样,进行更新并计算一个新的电子标签

发送成功回复。

204 No Content
E-Tag: "543"

如果出现另一个请求并尝试执行PUT与上述请求类似的请求,则服务器代码第二次评估它时,您有责任提供错误消息。

  • 确认电子标签仍然正确:“ 412”!=“ 543”

失败时,发送失败响应。

412 Precondition Failed

这是您实际上必须编写的代码。E-Tag实际上可以是任何文本(在HTTP规范中定义的限制内)。它不一定是数字。它也可以是哈希值。


这不是您在此处使用的标准HTTP表示法。在符合标准的HTTP中,仅在响应标头中使用ETag。您永远不会在请求标头中发送ETag,而是在请求标头中的If-Match或If-None-Match标头中使用以前获取的ETag值。
Lie Ryan

-2

作为其他答案的补充,我将在ZeroMQ文档中张贴最佳引用之一,以忠实地描述潜在问题:

为了制作完全完美的MT程序(从字面上看,我的意思是说),除了通过ZeroMQ套接字发送的消息外我们不需要互斥体,锁或任何其他形式的线程间通信。

“完美的MT程序”是指易于编写和理解的代码,可以在任何编程语言和任何操作系统上以相同的设计方法工作,并且可以在零等待状态和无意义的情况下跨任意数量的CPU进行扩展收益递减。

如果您花了数年的时间学习各种技巧,以使您的MT代码完全起作用,更不用说快速地使用锁,信号灯和关键部分了,那么当您意识到这一切都是徒劳的时,您会感到厌恶。如果我们从30多年的并发编程中汲取了教训,那就是:只是不共享状态。就像两个酒鬼试图共享啤酒。他们是好伙伴没关系。迟早他们会吵架。您添加到桌上的酒鬼越多,他们在啤酒上的争斗就越多。大多数MT应用程序的悲剧性看起来像是醉酒的打架。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.