84

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

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

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

4

10 回答 10

50

这就是我在 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()

上面列出的代码可以作为自定义管理器中的方法来实现。

我做出以下假设:

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

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

警告 Django 文档

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

于 2010-01-30T00:41:46.300 回答
40

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

select_for_update(nowait=True)

查看文档

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

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

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

于 2012-06-21T07:31:40.883 回答
30

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

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

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

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

为了实现乐观锁,当你保存数据时,你检查你从用户那里得到的版本是否与数据库中的版本相同,然后更新数据库并增加版本。如果不是,则意味着自加载数据以来发生了变化。

您可以通过单个 SQL 调用来做到这一点,例如:

UPDATE ... WHERE version = 'version_from_user';

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

于 2008-11-26T10:16:51.680 回答
14

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

于 2017-05-18T10:20:53.703 回答
3

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

于 2010-06-02T15:33:31.850 回答
1

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

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

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

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

于 2009-09-18T10:42:44.197 回答
0

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

于 2008-11-26T10:20:12.390 回答
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 这样的 modren 工具缺乏对 Optimictic Concurency Control 的指导。

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

于 2011-07-31T22:40:21.440 回答
-3

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

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

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

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

于 2008-11-26T09:23:16.373 回答
-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()
于 2009-12-09T15:52:14.677 回答