如何在Django中使用唯一检查来避免竞争条件

sie*_*iej 4 python mysql django postgresql validation

我有一个简单的模型:

class InvitationRequest(models.Model):
    email = models.EmailField(max_length=255, unique=True)
Run Code Online (Sandbox Code Playgroud)

一个简单的模型形式:

class InvitationRequestForm(forms.ModelForm):
    class Meta:
        model = InvitationRequest
Run Code Online (Sandbox Code Playgroud)

现在,假设我尝试以标准方式处理它:

form = InvitationRequestForm(request.POST)
if form.is_valid():
    form.save()
Run Code Online (Sandbox Code Playgroud)

存在竞争条件,因为验证执行简单SELECT查询以确定是否已经存储了这样的电子邮件,并且如果一切正常,则它继续进行form.save().如果有一个并发进程在同一时刻执行相同操作,则两个表单都将验证,并且两个进程都将调用,form.save()因此一个将成功,另一个将失败导致IntegrityError.

处理这个问题的标准方法是什么?

我希望在表单对象中有一个标准错误,因此我可以将其传递给模板并通知用户该问题.

我知道:

  • 我可以使用try/except包装所有内容并手动将新错误添加到我的表单中
  • 我可以用SERIALIZABLE事务包装所有东西(在MySQL中,因为它执行下一个键锁定每次选择)
  • 我可以使用覆盖Model._perform_unique_checks并使其使用select_for_update(由于下一个键锁定,与MySQL一起使用)
  • 我可以获得表级独占锁

这些解决方案都没有吸引力,我也使用PostgreSQL,它与MySQL在这个领域不同.

Tom*_*ski 8

标准方法是不处理这个,如:

  1. 你案件失败的概率接近于0;
  2. 失败的严重程度非常低.

如果出于某种原因,你必须确定问题不会发生,那么你就是靠自己.

我没有详细分析事件的顺序,但我认为使用SERIALIZABLE隔离级别不会真正有用,它只会导致IntegrityError(或DatabaseError)在不同的地方引发.

Model._perform_unique_checks如果可能的话,向我推翻声音就像一个坏主意一样,你最好远离猴子补丁(这里可能).

至于使用表锁来防止不可能出现的错误......好吧,我不是一个大粉丝所以我也不能推荐.

这是一个类似问题的一个很好的答案:https://stackoverflow.com/a/3523439/176186 - 我同意捕获IntegrityError和重试可能是处理问题最简单明智的方法.

编辑:我发现这个:Symfony2 - 如何在表单提交后从唯一约束错误中恢复?我同意@pid的回答.


Kev*_*nry 5

我同意 Tomasz Zielinski 的观点,通常的做法是不要担心这一点。对于大多数用例来说,这是不值得的。

如果它重要,最好的方法可能是乐观并发。在这种情况下,它可能看起来像(未经测试):

from django.forms.util import ErrorList

def handle_form(request)
    form = InvitationRequestForm(request.POST)
    try:
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(...)  # redirect to success url
    except IntegrityError:
        form._errors['email'] = ErrorList()
        form._errors['email'].append('Error msg') 

    return render(...)  # re-render the form with errors
Run Code Online (Sandbox Code Playgroud)

SERIALIZABLE在这里并没有真正的帮助。正如PostgreSQL 文档所明确的那样,您必须准备好处理序列化失败,这意味着代码看起来与上面几乎相同。(不过,如果您没有强制数据库抛出异常的约束,这会有所帮助。)unique