如何在模型的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)
有什么建议吗?
上述解决方案的另一个问题是,如果while
由IntegrityError
冲突(或任何其他唯一字段)引起,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
答案 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,因为一些保留未使用的位置)