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

时间:2008-11-26 09:00:35

标签: django django-models concurrency transactions atomic

是否有办法防止两个或更多用户同时修改同一数据库条目?

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

我认为锁定条目不是一个选项,因为用户可能会使用“后退”按钮或只是关闭他的浏览器,永远保持锁定。

10 个答案:

答案 0 :(得分:46)

这就是我在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()将导致单个数据库查询,因为过滤器是惰性的
  • 数据库查询是原子的

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

警告 Django Doc

  

请注意update()方法是   直接转换为SQL   声明。这是一个批量操作   直接更新。它没有运行任何   模型上的save()方法或发射   pre_save或post_save信号

答案 1 :(得分:35)

这个问题有点陈旧,我的回答有点晚了,但据我所知,这已经在Django 1.4 中修复了使用:

select_for_update(nowait=True)

请参阅docs

  

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

     

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

当然,这仅在后端支持“select for update”功能时才有效,例如sqlite不支持。不幸的是:MySql不支持nowait=True,您必须使用nowait=False,它只会在锁被释放之前阻止。

答案 2 :(得分:28)

实际上,事务在这里对你没什么帮助...除非你想让事务在多个HTTP请求上运行(你很可能不想要)。

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

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

  1. 读取数据并将其显示给用户
  2. 用户修改数据
  3. 用户发布数据
  4. 应用将其保存回数据库。
  5. 要实现乐观锁定,在保存数据时,检查从用户返回的版本是否与数据库中的版本相同,然后更新数据库并增加版本。如果不是,则表示自加载数据后发生了变化。

    您可以通过单个SQL调用执行此操作,例如:

    UPDATE ... WHERE version = 'version_from_user';
    

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

答案 3 :(得分:12)

Django 1.11有three convenient options来处理这种情况,具体取决于您的业务逻辑要求:

  • 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允许我默默地跳过重复的触发器。

答案 4 :(得分:3)

为了将来参考,请查看https://github.com/RobCombs/django-locking。它以一种不会留下永久锁定的方式锁定,通过用户离开页面时javascript解锁的混合,以及锁定超时(例如,在用户的浏览器崩溃的情况下)。文档非常完整。

答案 5 :(得分:1)

你应该至少使用django事务中间件,即使不管这个问题。

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

检查用户正在更新的版本(安全地执行此操作,因此用户不能简单地破解系统以说他们正在更新最新版本!),并且只有在该版本是最新版本时才会更新。否则,向用户返回一个新页面,其中包含他们正在编辑的原始版本,他们提交的版本以及其他人编写的新版本。要求他们将更改合并为一个完全最新的版本。您可能尝试使用像diff + patch这样的工具集自动合并它们,但无论如何您都需要使用手动合并方法来处理故障情况,所以从此开始。此外,您需要保留版本历史记录,并允许管理员还原更改,以防有人无意或故意搞砸合并。但无论如何你应该有这个。

很可能有一个django app / library为你做了大部分工作。

答案 6 :(得分:0)

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

答案 7 :(得分: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()

看起来很棒,即使没有可序列化的交易也应该可以正常工作。

问题是如何增加deafult .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这样的modren工具缺乏对Optimictic Concurency Control的指导,这很奇怪。

当我解开谜语时,我会更新这篇文章。希望解决方案将以一种不错的pythonic方式,不涉及大量的编码,奇怪的视图,跳过Django等基本部分。

答案 8 :(得分:-2)

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

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

如果字段是数字,例如帐户余额,商店中的商品数量等,如果您计算原始值(用户开始填写表格时存储的)与您可以开始交易的新值之间的差异,您可以更自动地处理它值并添加差异,然后结束交易。如果您不能有负值,则应该在结果为否定时中止事务,并告诉用户。

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

答案 9 :(得分:-6)

从这里:
How to prevent overwriting an object someone else has modified

我假设时间戳将作为隐藏字段保存在您尝试保存详细信息的表单中。

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()