问题描述
应该如何处理模型的 save()
方法中可能出现的竞争条件?
How should one handle a possible race condition in a model's save()
method?
例如,以下示例实现了一个具有相关项目有序列表的模型.创建新项目时,当前列表大小用作其位置.
For example, the following example implements a model with an ordered list of related items. When creating a new Item the current list size is used as its position.
据我所知,如果同时创建多个项目,这可能会出错.
From what I can tell, this can go wrong if multiple Items are created concurrently.
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()
但这似乎只适用于视图.即使它确实适用于模型方法,我仍然不知道如何正确处理失败的事务.
I've come across @transactions.commit_on_success()
but that seems to apply only to views. Even if it did apply to model methods, I still wouldn't know how to properly handle a failed transaction.
我目前正在处理它,但感觉更像是一个黑客而不是解决方案
I am currenly handling it like so, but it feels more like a hack than a solution
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
冲突(或任何其他唯一的领域).
Another problem with the above solution is that the while
loop will never end if IntegrityError
is caused by a name
conflict (or any other unique field for that matter).
为了记录,这是我目前所拥有的,似乎可以满足我的需要:
For the record, here's what I have so far which seems to do what I need:
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
推荐答案
对你来说可能感觉像一个黑客,但对我来说它看起来像是乐观并发"的合法、合理的实现方法——尝试做任何事情,检测由竞争条件引起的冲突,如果发生,稍后重试.一些数据库系统地使用它而不是锁定,它可以带来更好的性能,除非在写入负载很多的系统下(这在现实生活中非常罕见).
It may feel like a hack to you, but to me it looks like a legitimate, reasonable implementation of the "optimistic concurrency" approach -- try doing whatever, detect conflicts caused by race conditions, if one occurs, retry a bit later. Some databases systematically uses that instead of locking, and it can lead to much better performance except under systems under a lot of write-load (which are quite rare in real life).
我非常喜欢它,因为我认为它是 Hopper 原则的一般情况:请求宽恕比请求许可更容易",这在编程中广泛适用(尤其是但不仅限于 Python——Hopper 语言通常是毕竟,值得称赞的是 Cobol;-)
I like it a lot because I see it as a general case of the Hopper Principle: "it's easy to ask forgiveness than permission", which applies widely in programming (especially but not exclusively in Python -- the language Hopper is usually credited for is, after all, Cobol;-).
我建议的一项改进是等待随机时间——避免元竞争条件",即两个进程同时尝试,都发现冲突,然后都重试再次同时导致饥饿".time.sleep(random.uniform(0.1, 0.6))
之类的就足够了.
One improvement I'd recommend is to wait a random amount of time -- avoid a "meta-race condition" where two processes try at the same time, both find conflicts, and both retry again at the same time, leading to "starvation". time.sleep(random.uniform(0.1, 0.6))
or the like should suffice.
一个更精细的改进是在遇到更多冲突时延长预期的等待时间——这就是 TCP/IP 中所谓的指数退避"(您不必以指数方式延长事情,即通过一个常数乘数> 1,当然,但这种方法具有很好的数学特性).仅保证限制非常写入加载系统的问题(在尝试写入期间经常发生多次冲突),并且在您的特定情况下可能不值得.
A more refined improvement is to lengthen the expected wait if more conflicts are met -- this is what is known as "exponential backoff" in TCP/IP (you wouldn't have to lengthen things exponentially, i.e. by a constant multiplier > 1 each time, of course, but that approach has nice mathematical properties). It's only warranted to limit problems for very write-loaded systems (where multiple conflicts during attempted writes happen quite often) and it may likely not be worth it in your specific case.
这篇关于在 model.save() 中处理竞争条件的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!