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

Posted

技术标签:

【中文标题】在 model.save() 中处理竞争条件【英文标题】:Handling race condition in model.save() 【发布时间】:2011-04-01 04:07:06 【问题描述】:

在模型的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(),但这似乎只适用于视图。即使它确实适用于模型方法,我仍然不知道如何正确处理失败的事务。

我目前正在这样处理它,但感觉更像是一种 hack 而不是解决方案

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)

有什么建议吗?

更新:

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

为了记录,这是我目前所拥有的,似乎可以满足我的需要:

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

【问题讨论】:

【参考方案1】:

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

【讨论】:

【参考方案2】:

它可能感觉对你来说像是一个 hack,但对我来说,它看起来像是“乐观并发”方法的合法、合理的实现——尝试做任何事情,检测由竞争条件引起的冲突,如果发生,请稍后重试。一些数据库系统地使用它而不是锁定,并且它可以带来更好的性能,除非在系统下 lot 写入负载(这在现实生活中非常罕见)。

我非常喜欢它,因为我认为它是 Hopper 原则的一般案例:“请求宽恕比请求许可更容易”,这在编程中广泛应用(尤其是但不仅限于 Python——Hopper 语言通常是毕竟,值得称赞的是 Cobol;-)。

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

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

【讨论】:

谢谢亚历克斯。很高兴知道我没有在这棵树上叫错树。【参考方案3】:

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

self.position = self.parent.item_count

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

只是为了确保我处理的是最新的职位编号(在我的情况下,由于某些保留的未使用职位,这可能不是 item_count)

【讨论】:

以上是关于在 model.save() 中处理竞争条件的主要内容,如果未能解决你的问题,请参考以下文章

JVM 如何在内部处理竞争条件?

Python多处理竞争条件

处理事务数据库表中的竞争条件

分离线程中的竞争条件

使用 __block 的竞争条件

接口中固态的竞争条件