Django:如何防止数据库条目的并发修改


80

是否存在防止两个或多个用户同时修改同一数据库条目的方法?

向用户显示错误消息以执行第二次提交/保存操作是可以接受的,但是数据不应被静默覆盖。

我认为锁定条目不是一种选择,因为用户可能会使用“后退”按钮或只是关闭浏览器,从而永远保持锁定状态。


4
如果一个对象可以由多个并发用户更新,则可能会有更大的设计问题。可能需要考虑特定于用户的资源或将处理步骤分为单独的表以防止出现此问题。
S.Lott

Answers:


48

这就是我在Django中进行乐观锁定的方式:

updated = Entry.objects.filter(Q(id=e.id) && Q(version=e.version))\
          .update(updated_field=new_value, version=e.version+1)
if not updated:
    raise ConcurrentModificationException()

上面列出的代码可以作为Custom Manager中的方法实现。

我做出以下假设:

  • filter()。update()将导致单个数据库查询,因为filter是惰性的
  • 数据库查询是原子的

这些假设足以确保之前没有其他人更新过该条目。如果以这种方式更新了多行,则应使用事务。

警告 Django Doc

请注意,update()方法直接转换为SQL语句。这是直接更新的批量操作。它不会在模型上运行任何save()方法,也不会发出pre_save或post_save信号


12
真好!但是,这不是'&'而不是'&&'吗?
吉尔斯·托马斯

1
您可以通过将对“ update”的调用放在您自己的覆盖的save()方法内来回避“更新”未运行save()方法的问题吗?
乔纳森·哈特利

1
当两个线程并发调用时,两个线程filter都收到未经修改的相同列表e,然后又并发调用时会发生update什么?我看不到阻止同时过滤和更新的信号灯。编辑:哦,我现在了解惰性过滤器。但是,假设update()是原子的,这有什么用呢?数据库肯定会处理并发访问
totowtwo 2011年

1
@totowtwo ACID中的I保证排序(en.wikipedia.org/wiki/ACID)。如果UPDATE正在对与并发(但后来又开始执行)SELECT有关的数据执行,它将阻塞直到UPDATE完成。但是,可以同时执行多个SELECT。
Kit Sunde

1
看起来这仅在自动提交模式(默认)下才能正常工作。否则,最终的COMMIT将与此更新的SQL语句分开,因此并发代码可以在它们之间运行。而且我们在Django中具有ReadCommited隔离级别,因此它将读取旧版本。(为什么我要在这里进行手动交易-因为我想在此更新的同时在另一个表中创建一行。)但是,好主意。
Alex Lokk 2013年

39

这个问题有点老了,我的回答有点晚了,但是在我理解之后,在Django 1.4中使用以下命令解决了这个问题:

select_for_update(nowait=True)

文档

返回一个查询集,该查询集将锁定行直到事务结束,从而在支持的数据库上生成SELECT ... FOR UPDATE SQL语句。

通常,如果另一个事务已经获得了对所选行之一的锁定,则查询将阻塞,直到释放该锁定为止。如果这不是您想要的行为,请调用select_for_update(nowait = True)。这将使呼叫成为非阻塞。如果另一个事务已经获取了冲突的锁,则在评估查询集时将引发DatabaseError。

当然,这仅在后端支持“选择更新”功能(例如sqlite不支持)时才有效。不幸的是:nowait=TrueMySql不支持:您必须在其中使用:nowait=False,它只会在释放锁之前阻塞。


2
这不是一个很好的答案-这个问题明确地不想要(悲观的)锁定,并且出于这个原因,目前投票率最高的两个答案集中在乐观并发控制(“乐观锁定”)上。不过,“选择更新”在其他情况下也可以。
RichVel 2014年

@ giZm0这仍然使其悲观锁定。获得锁的第一个线程可以无限期持有它。
knaperek 2014年

6
我喜欢这个答案,因为它是Django的文档,而不是任何第三方的精美发明。
anizzomc

28

实际上,事务在这里无济于事...除非您希望使事务运行在多个HTTP请求上(您可能不希望这样做)。

在这些情况下,我们通常使用的是“乐观锁定”。据我所知,Django ORM不支持该功能。但是,已经有一些关于添加此功能的讨论。

所以你自己一个人。基本上,您应该做的是在模型中添加一个“版本”字段,并将其作为隐藏字段传递给用户。更新的正常周期为:

  1. 读取数据并将其显示给用户
  2. 用户修改数据
  3. 用户发布数据
  4. 该应用程序将其保存回数据库中。

为了实现乐观锁定,在保存数据时,您需要检查从用户那里获得的版本是否与数据库中的版本相同,然后更新数据库并递增版本。如果不是,则表示自加载数据以来发生了更改。

您可以通过单个SQL调用来实现,例如:

UPDATE ... WHERE version = 'version_from_user';

仅当版本相同时,此调用才会更新数据库。


1
同样的问题也出现在Slashdot上。您建议的“乐观锁”也在此处提出,但恕我直言解释得更好:hardware.slashdot.org/comments.pl?
sid=1381511&cid=29536367

5
还要注意,您确实想在此之上使用事务,以避免这种情况:hardware.slashdot.org/comments.pl? sid=1381511&cid=29536613 Django提供了中间件来自动在事务中包装数据库上的每个操作,从开始从初始请求开始并仅在成功响应后提交:docs.djangoproject.com/en/dev/topics/db/transactions (请注意:事务中间件仅有助于避免乐观锁定的上述问题,它不提供锁定本身)
Hopla

我还在寻找有关如何执行此操作的详细信息。到目前为止没有运气。
seanyboy 2009年

1
您可以使用django批量更新来完成此操作。检查我的答案。
Andrei Savu,2010年

13

Django 1.11提供了三个方便的选项来处理这种情况,具体取决于您的业务逻辑要求:

  • Something.objects.select_for_update() 将阻止,直到模型免费
  • Something.objects.select_for_update(nowait=True)并捕获DatabaseError模型当前是否已锁定以进行更新
  • Something.objects.select_for_update(skip_locked=True) 不会返回当前锁定的对象

在我的应用程序中,该应用程序在各种模型上都具有交互式和批处理工作流,我发现这三个选项可以解决大多数并发处理场景。

select_for_update在顺序批处理中,“等待”非常方便-我希望它们全部执行,但让它们花些时间。在nowait使用时用户要修改当前锁定用于更新的对象-我会告诉他们这是一个在这一刻被修改。

skip_locked是另一种类型的更新,当用户可以触发重新扫描对象的有用的-我不关心谁触发它,只要它的触发,所以skip_locked让我静静地跳过重复的触发器。


1
我是否需要使用transaction.atomic()包装要更新的选择?如果即时通讯实际使用结果进行更新?它不会锁定整个表,使select_for_update失效吗?
Paul Kenjora '19


1

即使不考虑此问题,您也可能至少应使用django事务中间件。

至于让多个用户编辑相同数据的实际问题,是的,请使用锁定。要么:

检查用户要针对哪个版本进行更新(安全地执行此操作,因此用户不能简单地破解系统以说他们正在更新最新副本!),并且仅在该版本为当前版本时才进行更新。否则,向用户发送一个新页面,其中包含他们正在编辑的原始版本,提交的版本以及其他人编写的新版本。要求他们将更改合并到一个完全最新的版本中。您可能会尝试使用diff + patch之类的工具集自动合并这些文件,但是无论如何,您都需要使手动合并方法适用于失败案例,因此从此开始。此外,您将需要保留版本历史记录,并允许管理员还原更改,以防有人无意或无意地破坏了合并。但是无论如何,您可能都应该拥有它。

django应用程序/库很可能为您完成了大部分工作。


就像纪尧姆(Guillaume)提出的那样,这也是乐观锁。但他似乎明白了所有要点:)
Hopla

0

要寻找的另一件事是“原子”一词。原子操作意味着您的数据库更改将成功发生或明显失败。快速搜索显示此问题,询问Django中的原子操作。


我不想在多个请求之间执行事务或锁定,因为这可能会花费任何时间(并且可能永远都不会完成)
Ber

如果事务开始,则必须完成。您仅应在用户单击“提交”后锁定记录(或开始交易,或您决定执行的任何操作),而不是在用户打开记​​录进行查看时锁定记录。
哈雷霍科姆

是的,但是我的问题不同,因为两个用户打开相同的表单,然后他们都提交了更改。我认为锁定不是解决方案。
误码率

您是对的,但是问题没有解决方案。一个用户赢得了胜利,另一个用户收到了失败消息。锁定记录的时间越晚,您遇到的问题就越少。
哈雷霍科姆

我同意。我完全接受其他用户的失败消息。我正在寻找一种检测这种情况的好方法(我希望这种情况很少见)。
误码率

0

上面的想法

updated = Entry.objects.filter(Q(id=e.id) && Q(version=e.version))\
      .update(updated_field=new_value, version=e.version+1)
if not updated:
      raise ConcurrentModificationException()

看起来不错,即使没有可序列化的事务也可以正常工作。

问题在于如何增强默认的.save()行为,而不必手动执行调用.update()方法的操作。

我看了自定义经理的想法。

我的计划是重写由Model.save_base()调用的Manager _update方法来执行更新。

这是Django 1.3中的当前代码

def _update(self, values, **kwargs):
   return self.get_query_set()._update(values, **kwargs)

恕我直言,需要做的事情是这样的:

def _update(self, values, **kwargs):
   #TODO Get version field value
   v = self.get_version_field_value(values[0])
   return self.get_query_set().filter(Q(version=v))._update(values, **kwargs)

删除时也需要发生类似的事情。但是,删除起来有点困难,因为Django通过django.db.models.deletion.Collector在该领域实现了很多巫毒教。

奇怪的是,像Django这样的现代工具缺少有关光学并发控制的指导。

解决谜题时,我将更新此帖子。希望解决方案将采用一种不错的pythonic方式,而不涉及大量的编码,怪异的视图,跳过Django的重要部分等。


-2

为了安全起见,数据库需要支持事务

如果字段是“自由格式”(例如文本等),并且您需要允许多个用户能够编辑相同的字段(您不能拥有数据的单个用户所有权),则可以将原始数据存储在变量。当用户提交时,检查输入数据是否已从原始数据更改(如果没有更改,则无需通过重写旧数据来打扰数据库),如果原始数据与数据库中的当前数据相比是相同的您可以保存,如果已更改,则可以向用户显示差异并询问用户该怎么做。

如果这些字段是数字,例如帐户余额,商店中的项目数等,则可以计算原始值(用户开始填写表格时存储的值)与新值之间的差,从而可以更自动地处理开始交易时,读取当前值并加上差额,然后结束交易。如果不能使用负值,则如果结果为负,则应中止事务,并告知用户。

我不知道django,所以我不能给你cod3s ..;)


-6

从这里开始:
如何防止覆盖别人修改过的对象

我假设时间戳记将以隐藏字段的形式保存在您要保存其详细信息的表单中。

def save(self):
    if(self.id):
        foo = Foo.objects.get(pk=self.id)
        if(foo.timestamp > self.timestamp):
            raise Exception, "trying to save outdated Foo" 
    super(Foo, self).save()

1
该代码已损坏。在if检查和保存查询之间仍然可能出现竞争条件。您需要使用objects.filter(id = ..&timestamp check).update(...)并在没有行更新的情况下引发异常。
Andrei Savu,2010年
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.