处理 model.save() 中的竞争条件
应如何处理模型的 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< /a>.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)
有什么建议吗?
更新:
上述解决方案的另一个问题是,如果 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
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)
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)
Any suggestions?
Update:
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
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
对你来说,它可能感觉像是黑客攻击,但对我来说,它看起来像是“乐观并发”方法的合法、合理的实现——尝试做任何事情,检测由竞争条件引起的冲突(如果发生) ,稍后重试。一些数据库系统地使用它而不是锁定,并且它可以带来更好的性能,除非系统处于大量写入负载(这在现实生活中很少见)。
我非常喜欢它,因为我将其视为霍珀原则的一般情况:“请求宽恕比请求许可更容易”,该原则广泛应用于编程(尤其但不限于 Python——霍珀通常认为是该语言的编程语言) ,毕竟是 Cobol;-)。
我建议的一项改进是等待一段随机时间——避免出现“元竞争条件”,即两个进程同时尝试,都发现冲突,然后都重试再次同时,导致“饥饿”。
time.sleep(random.uniform(0.1, 0.6))
或类似的内容就足够了。更精细的改进是,如果遇到更多冲突,则延长预期的等待时间——这就是 TCP/IP 中所谓的“指数退避”(您不必以指数方式延长时间,即通过常数乘数 > 1当然,每次都是如此,但这种方法具有很好的数学特性)。它只保证限制非常写入负载系统的问题(在尝试写入期间经常发生多个冲突),并且在您的特定情况下可能不值得。
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).
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;-).
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.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.
将可选的 FOR UPDATE 子句添加到查询集 http://code.djangoproject.com/ticket/2705
Add optional FOR UPDATE clause to QuerySets http://code.djangoproject.com/ticket/2705
我使用 Shawn Chin 的解决方案,事实证明它非常有用。我所做的唯一更改是将 替换
为
只是为了确保我正在处理最新的位置编号(在我的情况下可能不是 item_count 因为一些保留的未使用的位置)
I use Shawn Chin's solution and it proves very useful. The only change I did was to replace the
with
just to make sure I am dealing with the latest position number (which in my case might not be item_count because of some reserved unused positions)