【问题标题】:How do I deal with this race condition in django?如何在 django 中处理这种竞争条件?
【发布时间】:2011-01-15 04:08:50
【问题描述】:

此代码应该获取或创建一个对象并在必要时对其进行更新。该代码在网站上用于生产。

在某些情况下——当数据库繁忙时——它会抛出异常“DoesNotExist: MyObj 匹配查询不存在”。

# Model:
class MyObj(models.Model):
    thing = models.ForeignKey(Thing)
    owner = models.ForeignKey(User)
    state = models.BooleanField()
    class Meta:
        unique_together = (('thing', 'owner'),)

# Update or create myobj
@transaction.commit_on_success
def create_or_update_myobj(owner, thing, state)
    try:
        myobj, created = MyObj.objects.get_or_create(owner=user,thing=thing)

    except IntegrityError:
        myobj = MyObj.objects.get(owner=user,thing=thing)
        # Will sometimes throw "DoesNotExist: MyObj matching query does not exist"

    myobj.state = state
    myobj.save()

我在 ubuntu 上使用了一个 innodb mysql 数据库。

如何安全地处理这个问题?

【问题讨论】:

    标签: django innodb


    【解决方案1】:

    这可能是与此处相同的问题的一个分支:

    Why doesn't this loop display an updated object count every five seconds?

    基本上get_or_create 可能会失败 - 如果你看看它的来源,你会发现它是:get, if-problem: save+some_trickery, if-still-problem:再次获得,如果仍然有问题:投降并提高。

    这意味着如果有两个同时运行的线程(或进程)create_or_update_myobj,都试图 get_or_create 同一个对象,那么:

    • 第一个线程尝试获取它 - 但它还不存在,
    • 所以,线程尝试创建它,但在创建对象之前...
    • ...第二个线程尝试获取它 - 这显然失败了
    • 现在,由于 MySQLdb 数据库连接的默认 AUTOCOMMIT=OFF 和 REPEATABLE READ 可序列化级别,两个线程都冻结了它们对 MyObj 表的视图。
    • 随后,第一个线程创建它的对象并优雅地返回它,但是...
    • ...第二个线程不能创建任何东西,因为它会违反unique 约束
    • 有趣的是,由于 MyObj 表的冻结视图,第二个线程上的后续 get 看不到在第一个线程中创建的对象

    所以,如果您想安全地get_or_create 任何事情,请尝试以下操作:

     @transaction.commit_on_success
     def my_get_or_create(...):
         try:
             obj = MyObj.objects.create(...)
         except IntegrityError:
             transaction.commit()
             obj = MyObj.objects.get(...)
         return obj
    

    于 2010 年 5 月 27 日编辑

    还有第二种解决方案 - 使用 READ COMMITED 隔离级别,而不是 REPEATABLE READ。但它的测试较少(至少在 MySQL 中),因此它可能存在更多错误/问题 - 但至少它允许将视图绑定到事务,而无需在中间提交。

    于 2012 年 1 月 22 日编辑

    这里有一些关于 MySQL 和 Django 的好博文(不是我的),与这个问题相关:

    http://www.no-ack.org/2010/07/mysql-transactions-and-django.html

    http://www.no-ack.org/2011/05/broken-transaction-management-in-mysql.html

    【讨论】:

    • 你是绝对正确的。提交事务解决了这个问题。谢谢:-)
    • 是否有一个补丁返回到 django 的 get_or_create 等待发生在这里?
    • code.djangoproject.com/ticket/13906之类的票但问题不小。
    • 现在链接好像坏了:(
    • 这种竞争条件是 mysql 特有的吗? postgres 会遇到同样的问题吗?
    【解决方案2】:

    您的异常处理正在掩盖错误。您应该在get_or_create() 中传递state 的值,或者在模型和数据库中设置默认值。

    【讨论】:

    • 在我运行 create_or_update_myobj 时,“所有者”可能已经拥有处于不同“状态”的“事物”。在这种情况下,我需要获取现有的“事物”并更改“状态”。
    • 或者它可能没有 any 状态,因为没有这样的记录,此时它会尝试创建新记录,此时它会立即内爆。
    • 有趣,虽然您的博客是私人的,所以无法阅读帖子。
    • @Hobhouse @IgnacioVazquez-Abrams 你们都说对了一半。您需要使用默认值 kwarg docs.djangoproject.com/en/dev/ref/models/querysets/… 传递 state
    【解决方案3】:

    一种(愚蠢的)方法可能是捕获错误并在等待一小段时间后简单地重试一次或两次。我不是数据库专家,所以可能有信号解决方案。

    【讨论】:

      【解决方案4】:

      自 2012 年以来,在 Django 中,我们有 select_for_update 锁定行直到事务结束。

      避免 Django + MySQL 中的竞争条件 默认情况下:

      • Mysql 中的 REPEATABLE_READ
      • Django 中的READ_COMMITTED

      你可以用这个:

      with transaction.atomic():
         instance = YourModel.objects.select_for_update().get(id=42)
         instance.evolve()
         instance.save()
      

      第二个线程会等待第一个线程(锁),只有第一个线程完成后,第二个线程才会读取第一个线程保存的数据,所以它会处理更新的数据。

      然后和get_or_create一起:

      def select_for_update_or_create(...):
          instance = YourModel.objects.filter(
              ...
          ).select_for_update().first()
      
          if order is None:
              instnace = YouModel.objects.create(...)
      
          return instance
      

      函数必须在事务块内,否则,你会从 Django 中得到: TransactionManagementError: select_for_update 不能在事务之外使用


      有时使用refresh_from_db() 也很好 如:

      instance = YourModel.objects.create(**kwargs)
      response = do_request_which_lasts_few_seconds(instance)
      instance.attr = response.something
      

      你想看:

      instance = MyModel.objects.create(**kwargs)
      response = do_request_which_lasts_few_seconds(instance)
      instance.refresh_from_db()  # 3
      instance.attr = response.something
      

      #3 将大大减少可能的竞争条件的时间窗口,因此有机会。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-04-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-01-08
        • 2016-12-10
        • 2011-04-01
        • 2014-02-23
        相关资源
        最近更新 更多