处理model.save()中的竞争条件

时间:2010-08-19 14:32:58

标签: python database django django-models race-condition

如何在模型的save()方法中处理可能的竞争条件?

例如,以下示例使用相关项的有序列表实现模型。创建新项目时,当前列表大小用作其位置。

据我所知,如果同时创建多个项目,这可能会出错。

class OrderedList(models.Model):
    # ....
    @property
    def item_count(self):
        return self.item_set.count()

class Item(models.Model):
    # ...
    name   = models.CharField(max_length=100)
    parent = models.ForeignKey(OrderedList)
    position = models.IntegerField()
    class Meta:
        unique_together = (('parent','position'), ('parent', 'name'))

    def save(self, *args, **kwargs):
        if not self.id:
            # use item count as next position number
            self.position = parent.item_count
        super(Item, self).save(*args, **kwargs)

我遇到过@transactions .commit_on_success(),但这似乎只适用于观看次数。即使它确实适用于模型方法,我仍然不知道如何正确处理失败的事务。

我当前正在处理它,但感觉更像是黑客而不是解决方案

def save(self, *args, **kwargs):
    while not self.id:
        try:
            self.position = self.parent.item_count
            super(Item, self).save(*args, **kwargs)
        except IntegrityError:
            # chill out, then try again
            time.sleep(0.5)

有什么建议吗?

更新

上述解决方案的另一个问题是,如果whileIntegrityError冲突(或任何其他唯一字段)引起,name循环将永远不会结束。

为了记录,这是我到目前为止所做的事情,似乎做了我需要的事情:

def save(self, *args, **kwargs):   
    # for object update, do the usual save     
    if self.id: 
        super(Step, self).save(*args, **kwargs)
        return

    # for object creation, assign a unique position
    while not self.id:
        try:
            self.position = self.parent.item_count
            super(Step, self).save(*args, **kwargs)
        except IntegrityError:
            try:
                rival = self.parent.item_set.get(position=self.position)
            except ObjectDoesNotExist: # not a conflict on "position"
                raise IntegrityError
            else:
                sleep(random.uniform(0.5, 1)) # chill out, then try again

3 个答案:

答案 0 :(得分:15)

它可能感觉就像对你的黑客攻击一样,但对我而言,它似乎是“乐观并发”方法的合法,合理的实现 - 尝试做任何事情,检测由竞争条件引起的冲突,如果发生,请稍后重试。有些数据库系统地使用它而不是锁定,它可以带来更好的性能,除非在批次写入负载下的系统(在现实生活中很少见)。

我非常喜欢它,因为我认为它是Hopper原则的一般情况:“很容易请求宽恕而非许可”,这在编程中广泛适用(特别是但不仅限于Python) - Hopper通常是语言毕竟,考虑到了Cobol; - )。

我建议的一个改进是等待随机一段时间 - 避免两个进程同时尝试的“元竞争条件”,两者都发现冲突,并且都重试再次 ,导致“饥饿”。 time.sleep(random.uniform(0.1, 0.6))等就足够了。

如果遇到更多冲突,更精确的改进是延长预期等待 - 这就是TCP / IP中所谓的“指数退避”(您不必以指数方式延长事物,即通过常数乘数当然,每次都是> 1,但这种方法具有很好的数学特性)。仅保证限制非常写入加载系统的问题(在尝试写入期间发生多次冲突的情况下),并且在特定情况下可能不值得。

答案 1 :(得分:0)

将可选的FOR UPDATE子句添加到QuerySets http://code.djangoproject.com/ticket/2705

答案 2 :(得分:0)

我使用Shawn Chin的解决方案,它证明非常有用。我做的唯一改变是替换

self.position = self.parent.item_count

self.position = self.parent.latest('position').position

只是为了确保我正在处理最新的位置编号(在我的情况下可能不是item_count,因为一些保留未使用的位置)