有没有办法防止两个或多个用户同时修改同一数据库条目?
向执行第二次提交/保存操作的用户显示错误消息是可以接受的,但不应该默默地覆盖数据。
我认为锁定条目不是一个选项,因为用户可能使用“返回”按钮或简单地关闭浏览器,永远留下锁定。
有没有办法防止两个或多个用户同时修改同一数据库条目?
向执行第二次提交/保存操作的用户显示错误消息是可以接受的,但不应该默默地覆盖数据。
我认为锁定条目不是一个选项,因为用户可能使用“返回”按钮或简单地关闭浏览器,永远留下锁定。
这是我在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()
上面列出的代码可以被实现为自定义管理器中的一种方法。请注意,update()方法直接转换为SQL语句。 它是用于直接更新的批量操作。 它不运行任何save()方法,也不发出pre_save或post_save信号。
filter
时会发生什么情况?它们都会收到一个未修改的相同列表e
,然后同时调用update
。我没有看到任何阻止filter
和update
同时进行的信号量。编辑:哦,我现在理解了惰性过滤器。但是假设update()
是原子的有效性是什么?肯定数据库处理并发访问。 - totowtwo这个问题有点老了,我的回答也晚了一些,但根据我的理解,这个问题在Django 1.4中 已经得到修复,具体方法如下:
select_for_update(nowait=True)
请参阅文档
返回一个查询集,它将锁定行直到事务结束,在支持的数据库上生成SELECT ... FOR UPDATE SQL语句。
通常情况下,如果另一个事务已经锁定了所选行中的一行,则查询将阻塞,直到锁释放。如果这不是您想要的行为,请调用select_for_update(nowait=True)。这将使调用非阻塞。如果另一个事务已经获取了冲突的锁,则在查询集求值时会引发DatabaseError异常。
当然,这只在后端支持“select for update”功能时才有效,例如sqlite并不支持。不幸的是:nowait=True
在MySql中不被支持,您需要使用nowait=False
,这将仅在锁被释放之前阻塞。
实际上,在这里交易并没有帮助太多...除非你想要在多个HTTP请求之间运行事务(你可能不想这样做)。
在这种情况下,我们通常使用"乐观锁定"。据我所知,Django ORM不支持该功能。但是已经有一些关于添加此功能的讨论。
因此,您需要自己解决。基本上,您应该在模型中添加一个"版本"字段,并将其作为隐藏字段传递给用户。更新的正常周期如下:
要实现乐观锁定,保存数据时,请检查从用户获取的版本是否与数据库中的版本相同,然后更新数据库并增加版本。如果它们不同,则表示自加载数据以来发生了更改。
您可以使用类似以下内容的单个SQL调用来执行此操作:
UPDATE ... WHERE version = 'version_from_user';
仅当版本仍然相同时,此调用才会更新数据库。
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
允许我静默跳过重复的触发器。
供以后参考,请查看https://github.com/RobCombs/django-locking。它通过混合使用javascript解锁和锁定超时的方式进行锁定,不会留下永久锁定。文档非常完整。
无论如何,您应该至少使用django事务中间件,即使不考虑这个问题。
至于您实际遇到的多个用户编辑相同数据的问题...是的,请使用锁定。或者:
检查用户正在更新的版本(要安全地执行此操作,以防止用户简单地黑掉系统以表示他们正在更新最新副本!),并仅在该版本为当前版本时才进行更新。否则,向用户发送一个新页面,其中包含他们正在编辑的原始版本、提交的版本和其他人编写的新版本。请他们将更改合并为一个完全更新的版本。您可以尝试使用类似diff+patch的工具集自动合并这些内容,但是您需要让手动合并方法在失败情况下起作用,因此请从那里开始。此外,您需要保留版本历史记录,并允许管理员还原更改,以防某人无意或有意破坏合并。但是您可能应该一直这样做。
很可能有一个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()
看起来很不错,即使没有可序列化事务也应该能正常工作。
问题在于如何增强默认的 .save() 行为,以避免手动调用 .update() 方法进行管道操作。
我研究了自定义管理器的想法。
我的计划是覆盖 Manager _update 方法,该方法由 Model.save_base() 调用以执行更新操作。
这是 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的关键部分等。
为确保安全,数据库需要支持 事务。
如果字段是“自由格式”(例如文本等),并且需要允许多个用户能够编辑相同的字段(不能将数据归属于单个用户),则可以将原始数据存储在一个变量中。当用户提交时,检查输入数据是否已经与原始数据发生改变(如果没有,则不需要通过重写旧数据来打扰数据库),如果与数据库中的当前数据相比较,原始数据相同,则可以保存;如果原始数据发生了改变,则可以向用户显示差异并询问用户应该怎么做。
如果字段是数字,例如帐户余额、商店中的物品数量等,则可以更自动地处理。如果计算新值与原始值之间的差异,可以启动事务读取当前值并添加差异,然后结束事务。如果不能有负值,则如果结果为负数,应中止事务并告知用户。
我不知道 Django,所以无法提供代码... ;)
从这里开始:
如何防止他人修改的对象被覆盖
我假设时间戳将作为表单中的隐藏字段保存,以保存详细信息。
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()