防止 Django 在并发请求时将同一个对象多次保存到数据库中

Posted

技术标签:

【中文标题】防止 Django 在并发请求时将同一个对象多次保存到数据库中【英文标题】:Prevent Django from saving the same object multiple times to the database on concurrent requests 【发布时间】:2012-09-13 22:45:12 【问题描述】:

对于我们的博客平台,我们有一个“文章”模型,其中包含一个“更新的”日期时间字段:

class Article(models.Model):
    updated = models.DateTimeField(null=True, blank=True)
    ...

当任何访问者在 24 小时内第一次打开文章时,我们会对不同的模型字段进行一些耗时的计算,然后将模型保存到数据库中。有了这个,我们还将我们的“更新”字段更新为当前的 datetime.now()。

if (datetime.now() - article.updated).days > 1:
    # do some time consuming calculations
    article.updated = datetime.now()
    article.save()

当一篇文章或多或少同时被请求时,第一次请求的耗时操作还没有完成,导致每天一次的操作在同一个对象上重新开始(article.updated 仍然是旧的)价值)。在开始计算之前另外调用 article.save() 是否有帮助?还是该数据推迟到请求完成后才保存到数据库?

【问题讨论】:

【参考方案1】:

使用 Django 1.4 中引入的查询集select_for_update,它在数据库中执行行级锁定。所有匹配的条目都将被锁定,直到事务块结束,这意味着其他事务将被阻止更改或获取对它们的锁定。有一些特定于 datgabase 后端的陷阱,因此请确保在完全依赖它之前阅读并测试它。

独立于实现的其他一些方法是自定义模型以具有locked 布尔属性。不是很整洁,但一个可行的解决方案。见What is the simplest way to lock an object in Django

【讨论】:

感谢普拉蒂克!我们正在尝试 select_for_update() ... 给我们几天时间看看它是否有效。并发请求每两天发生一次...【参考方案2】:

一些建议:

最好将耗时的计算从请求-响应周期转移到后台。这里可以使用消息队列(如流行的celery)。我认为这是最好的解决方案,但它可能需要一些额外的管理,这对于简单的任务来说可能是多余的; 如果您使用缓存,您可以设置一个标志,表明对象被锁定。如果缓存对于不同的解释器(如 memcached)是通用的,那么即使您有许多运行您的应用的 Python 解释器,它也可以工作; 您可以安排更新过程(使用 cron 和自定义 Django 管理命令)来更新 >24 小时前更新的所有对象。除非您有大量的对象和大量的处理时间,否则它会起作用。

【讨论】:

谢谢,我喜欢并感谢您的建议。然而,我们确实有大量的对象,这就是为什么我们必须用这个“实时”系统替换我们的 cronjob :) cronjob 必须运行得如此频繁,以至于它不再有意义了... @Nasmon,那么消息队列将是最好的解决方案,如果你同意花一些时间来设置它:) 我已将 Celery 列入我们的待办事项清单……听起来真的很棒!目前,作为一种快速解决方案,select_for_update 效果很好。【参考方案3】:

短版:

@transaction.commit_on_success
def update_article( article_id ):
    article = Article.objects.select_for_update().get( pk = article_id )
    if (datetime.now() - article.updated).days > 1:
        # do some time consuming calculations
        article.updated = datetime.now()
        article.save()

select_for_update() 锁定数据库行(ID 为 article_ID 的文章)。 该行在事务结束时解锁,即 函数结束,因为 update_article()@transaction.commit_on_success 包裹。

附言 : 从 Django 1.4 开始可用

【讨论】:

"select_for_update" (已接受的答案)确实有效。但是很高兴有另一种方法。谢谢:-) thx ;) ...但实际上我认为没有@transaction.commit_on_success 装饰器select_for_update() 将无法工作。 select_for_update() 应该只在事务中有意义。这就是为什么我还添加了这个答案...欢迎更多 cmets 确认这一点! 我们的项目代码从那时起发生了很大变化,所以 - 不幸的是 - 我无法确认。但是,Django 的文档没有提到装饰器要求:docs.djangoproject.com/en/dev/ref/models/querysets/… ... @Nasmon: 不,事实上,他们没有提到装饰器而是事务,因为你可以以不同的方式实现事务(其中一种是装饰器)......我发现了昨天,没有装饰器 select_for_update 无法正常工作!

以上是关于防止 Django 在并发请求时将同一个对象多次保存到数据库中的主要内容,如果未能解决你的问题,请参考以下文章

使用Redis实现接口防重复提交

使用Redis实现接口防重复提交

防止在 Angular 中多次触发令牌刷新请求

后端处理高并发状态的多次重复请求

在 Django admin 中编辑组对象时将用户对象分配给组

$.ajax防止多次点击重复提交的方法