如何防止Web应用程序中的竞争状况?


31

考虑一个电子商务网站,其中Alice和Bob都在编辑产品清单。爱丽丝(Alice)正在改善描述,而鲍勃(Bob)正在更新价格。他们开始同时编辑Acme Wonder Widget。鲍勃首先完成并用新价格保存产品。爱丽丝花了更长的时间来更新描述,完成后,她用新描述保存产品。不幸的是,她还用原先的价格覆盖了价格,这是不希望的。

以我的经验,这些问题在Web应用程序中极为常见。某些软件(例如Wiki软件)确实对此具有保护作用-通常第二次保存失败,并显示“页面在编辑时已更新”。但是大多数网站都没有这种保护。

值得注意的是,控制器方法本身就是线程安全的。通常,他们使用数据库事务,从某种意义上说,如果爱丽丝和鲍勃试图在同一时刻进行保存,就不会造成损坏,这使它们变得安全。竞争条件是由Alice或Bob在浏览器中拥有过时的数据引起的。

我们如何预防这种比赛条件?我特别想知道:

  • 可以使用哪些技术?例如,跟踪上次更改的时间。各自的优缺点是什么。
  • 什么是有用的用户体验?
  • 此保护内置哪些框架?

您已经给出了答案:通过跟踪对象的更改日期并将其与其他更改尝试更新的数据的使用期限进行比较。您是否想知道其他内容,例如如何高效地做到这一点?
Kilian Foth,2014年

@KilianFoth-我添加了一些我特别想知道的信息
paj28

1
您的问题绝非Web应用程序所特有,桌面应用程序可能有完全相同的问题。典型的解决策略描述如下:stackoverflow.com/questions/129329/...
布朗博士

2
仅供参考,您在问题中提到的锁定形式称为“ 乐观并发控制
TehShrike 2014年

一些与Django有关的讨论在这里
paj28

Answers:


17

您需要“读取您的写内容”,这意味着在写下更改之前,您需要再次读取记录,并检查自上次读取记录以来对记录进行了任何更改。您可以逐字段(细粒度)或基于时间戳(粗粒度)进行此操作。进行此检查时,您需要对记录进行排他锁定。如果未进行任何更改,则可以记下您的更改并释放锁。如果在此期间记录已更改,则中止事务,释放锁并通知用户。


这听起来是最实用的方法。您知道实现此目的的任何框架吗?我认为此方案的最大问题是,简单的“编辑冲突”消息会使用户感到沮丧,但是尝试(手动或自动)合并变更集很困难。
paj28 2014年

不幸的是,我不知道有任何支持此功能的框架。我不认为只要没有经常出现编辑冲突错误消息就不会令人沮丧。最终,这取决于系统的用户负载,无论您是检查时间戳还是实现更复杂的合并功能。
Phil

我维护了一个PC分布式数据库产品,该产品使用细粒度的方法(针对其本地数据库副本):如果一个用户更改了价格而另一个用户更改了说明-没问题!就像现实生活中一样。如果两个用户更改了价格-第二个用户道歉并再次尝试更改。没问题!除了在将数据写入数据库的那一刻,这不需要锁。如果一个用户在屏幕上显示自己的零钱去吃午餐,然后再提交,则没关系。对于远程数据库更改,它基于记录时间戳。

1
Dataflex有一个称为“ reread()”的函数,它可以完成您所描述的事情。在更高版本中,它在多用户环境中是安全的。而且,确实,这是使这种交错更新生效的唯一方法。

您能举一个使用sql server的例子吗?\
l --''''''---------''''''''''''

10

我已经看到2种主要方式:

  1. 在隐藏的输入中添加用户正在编辑的页面的最新更新的时间戳。提交时间戳时,将对照当前时间戳进行检查,如果时间戳不匹配,则其他人已对其进行更新并返回错误。

    • 优点:多个用户可以编辑页面的不同部分。错误页面可能会导致差异页面,第二个用户可以在该页面将其更改合并到新页面中。

    • 缺点:有时在进行大量并发编辑时会浪费大量精力。

  2. 当用户在合理的时间内开始编辑页面锁时,另一个用户随后尝试编辑该页面时,他将得到一个错误页面,并且必须等待直到锁过期或第一个用户已提交。

    • 优点:编辑工作不会浪费。

    • 缺点:不道德的用户可以无限期锁定页面。除非另有处理(使用技术1),否则具有过期锁的页面可能仍然可以提交。


7

使用乐观并发控制

将versionNumber或versionTimestamp列添加到有问题的表中(整数最安全)。

用户1读取记录:

{id:1, status:unimportant, version:5}

用户2读取记录:

{id:1, status:unimportant, version:5}

用户1保存记录,这将增加版本:

save {id:1, status:important, version:5}
new value {id:1, status:important, version:6}

用户2尝试保存他们读取的记录:

save {id:1, status:unimportant, version:5}
ERROR

Hibernate / JPA可以通过@Version注释自动执行此操作

您通常需要在会话中的某个地方维护读取记录的状态(这比隐藏形式的变量更安全)。


谢谢...了解@Version特别有用。一个问题:为什么在会话中存储状态是安全的?在那种情况下,我会担心使用后退按钮会造成混乱。
paj28 2014年

会话比隐藏的表单元素更安全,因为用户将无法更改版本值。如果这不是一个问题,那么请忽略有关会议的部分
Neil McGuigan 2014年

此技术称为“ 乐观离线锁定”,它也在SQLAlchemy 中使用
paj28 2014年

@ paj28-的链接SQLAlchemy未指向有关乐观脱机锁的任何内容,而我在文档中找不到它。您是否有一个更有用的链接,或者只是将人们通常指向SQLAlchemy?
dwanderson

@dwanderson我的意思是该链接的版本计数器部分。
paj28年

1

某些对象关系映射(ORM)系统将检测对象的哪些字段自从数据库加载后已更改,并将构造SQL更新语句以仅设置这些值。Ruby on Rails的ActiveRecord就是这样的ORM。

最终的影响是,用户未更改的字段未包含在发送给数据库的UPDATE命令中。同时更新不同字段的人不会互相覆盖更改。

根据您使用的编程语言,研究可用的ORM,并查看它们中的任何一个是否只会更新应用程序中标记为“脏”的数据库中的列。


嗨,格雷格。不幸的是,这对这类比赛条件没有帮助。如果考虑我的原始示例,那么当Alice保存时,ORM会将price列视为脏列并进行更新-即使不需要更新。
paj28 2014年

1
@ paj28 Greg答案中的关键点是“ 用户未更改的字段 ”。爱丽丝没有更改价格,因此ORM不会尝试将“价格”值保存到数据库中。
罗斯·帕特森

@RossPatterson-ORM如何知道用户更改的字段与浏览器中的过期数据之间的区别?至少在没有进行额外跟踪的情况下,它不会。如果您想编辑Greg的答案以包括此类跟踪,或提交其他答案,那将很有帮助。
paj28 2014年

@ paj28系统的某些部分必须知道用户的操作,并且只存储用户所做的更改。如果用户更改了价格,然后又将其更改回然后提交了,则这不应算作“用户更改的内容”,因为它们没有更改。如果您的系统需要这种级别的并发控制,则必须以这种方式进行构建。如果没有,那就没有。

@nocomprende-可以肯定的是,有一部分-但是答案没有说ORM
paj28
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.