2008-11-26 187 views
70

如果有防止同时修改两个或更多用户的同一数据库条目的方法?Django:如何防止数据库条目的并发修改

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

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

+4

如果一个对象可以被多个并发用户更新,则可能会有更大的设计问题。可能值得考虑用户特定的资源,或将处理步骤分成单独的表格以防止这成为问题。 – 2008-11-26 17:03:46

回答

-1

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

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

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

我不知道Django的,所以我不能给你德cod3s ..;)

27

其实,交易不帮助你在这里多...除非你想有运行在多笔交易HTTP请求(你可能不想要)。

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

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

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

要实现乐观锁定,当您保存数据时,检查从用户处获得的版本是否与数据库中的版本相同,然后更新数据库并增加版本。如果不是,则意味着数据加载后发生了变化。

你可以做到这一点的东西,如一个单一的SQL调用:

UPDATE ... WHERE version = 'version_from_user'; 

只有在版本仍然是相同的这个调用将更新数据库。

+1

同样的问题也出现在Slashdot上。您建议的乐观锁定也是在那里提出的,但是解释了一点更好的imho:http://hardware.slashdot.org/comments.pl?sid=1381511&cid=29536367 – hopla 2009-09-29 09:35:21

+5

另外请注意,您确实希望在此之上使用事务,以避免出现这种情况:http://hardware.slashdot.org/comments.pl?sid=1381511&cid=29536613 Django提供中间件,以自动将事务中的数据库上的每个操作都包装在一起,从初始请求开始并仅提交在成功回应之后:http://docs.djangoproject.com/en/dev/topics/db/transactions/ (介意你:事务中间件只有帮助避免上述乐观锁定问题,它不提供锁定本身) – hopla 2009-09-29 12:12:58

+0

我也在寻找如何做到这一点的细节。到目前为止没有运气。 – seanyboy 2009-12-09 15:48:32

0

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

+0

我不想在多个请求中执行交易或锁定,因为这可能需要很长时间(并且可能永远不会完成) – Ber 2008-11-26 10:57:06

+0

如果交易开始,它必须完成。在用户点击“提交”之后,您应该只锁定记录(或开始交易,或者您决定执行的任何操作),而不是在他们打开记录进行查看时。 – 2008-11-26 11:01:20

+0

是的,但我的问题是不同的,因为两个用户打开相同的表单,然后他们都提交他们的更改。我不认为锁定是解决方案。 – Ber 2008-11-26 16:24:53

1

你或许应该至少使用Django的交易中间件,甚至不管这个问题。

对于具有多个用户编辑同一数据......是的,使用锁定的实际问题。 OR:

检查什么版本的用户更新是针对(这样做安全,因此用户不能简单地破解系统来说,他们更新最新的副本!),如果该版本是目前唯一的更新。否则,请向用户返回一个新页面,其中包含他们正在编辑的原始版本,他们提交的版本以及其他人编写的新版本。请他们将这些更改合并到一个完全更新的版本中。你可能会尝试自动合并使用这些差异一样补丁+工具集,但你需要有手动合并方法失败的案例工作,无论如何,所以着手行动。此外,您需要保留版本历史记录,并允许管理员恢复更改,以防有人无意或故意混淆合并。但是,无论如何你应该可以拥有。

有很可能一个Django应用程序/库,做这个最适合你。

43

这是怎么做乐观锁在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的方法来实现。

我提出如下假设:

  • 滤波器()update()方法会导致一个单一的数据库查询,因为过滤器是懒惰
  • 数据库查询是原子

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

警告Django Doc

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

3

以供将来参考,请查阅https://github.com/RobCombs/django-locking。它锁定的方式在用户离开网页时不留下永远的锁,由JavaScript解锁的混合物,并锁定超时(例如,如果用户的浏览器崩溃)。文档非常完整。

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 Concuncy Control的指导。

我会在解决谜题时更新这篇文章。希望解决方案将以一种很好的pythonic方式,不涉及吨编码,奇怪的意见,跳过Django的基本片断等。

33

这个问题有点老,我的答案有点晚,但在我明白这一点后已一直使用固定在Django 1.4

select_for_update(nowait=True) 

看到docs

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

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

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

6

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可以让我静静地跳过复制触发器。