在 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() 中处理竞争条件的主要内容,如果未能解决你的问题,请参考以下文章