如何在Django中避免唯一检查的竞争条件

11 投票
2 回答
3510 浏览
提问于 2025-04-20 04:58

我有一个简单的模型:

class InvitationRequest(models.Model):
    email = models.EmailField(max_length=255, unique=True)

还有一个简单的模型表单:

class InvitationRequestForm(forms.ModelForm):
    class Meta:
        model = InvitationRequest

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

form = InvitationRequestForm(request.POST)
if form.is_valid():
    form.save()

这里出现了一个竞争条件,因为验证会执行一个简单的 SELECT 查询来检查这个邮箱是否已经存在。如果一切正常,它就会继续到 form.save() 这一行。如果此时有另一个进程也在做同样的事情,那么两个表单都会通过验证,并且两个进程都会调用 form.save(),这样就会有一个成功,另一个失败,导致出现 IntegrityError 错误。

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

我想在表单对象中有一个标准的错误,这样我可以把它传递给模板,通知用户出现了问题。

我知道:

  • 我可以用 try/except 把所有内容包裹起来,然后手动给我的表单添加新的错误信息
  • 我可以用 SERIALIZABLE 事务来包裹所有操作(在 MySQL 中,因为它会对每个选择执行下一键锁定)
  • 我可以重写 Model._perform_unique_checks 方法,让它使用 select_for_update(在 MySQL 中有效,因为它也会进行下一键锁定)
  • 我可以获取表级的独占锁

但这些解决方案都不太吸引我,而且我使用的是 PostgreSQL,它在这方面和 MySQL 不一样。

2 个回答

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

SERIALIZABLE 在这里其实帮不上什么忙。正如PostgreSQL的文档所说明的,你需要准备好处理序列化失败的情况,这意味着代码看起来基本上还是和上面的一样。不过,如果你没有那个unique约束强制数据库抛出异常的话,情况会好一些。

13

通常情况下,我们不需要去处理这个问题,因为:

  1. 在你的情况下,出错的可能性几乎为零;
  2. 即使出错,影响也很小。

如果因为某种原因你必须确保这个问题不会发生,那就得自己想办法了。

我没有详细分析事件的顺序,但我觉得使用SERIALIZABLE隔离级别并不会真正解决问题,只会在其他地方引发IntegrityError(或DatabaseError)。

重写Model._perform_unique_checks听起来不是个好主意,尽量避免“猴子补丁”(也就是随意修改现有代码),在这里是可以避免的。

至于使用表锁来防止不太可能发生的错误……我对此不太感冒,所以也不推荐这种做法。

这里有个不错的回答,针对类似的问题:https://stackoverflow.com/a/3523439/176186 - 我同意,捕获IntegrityError并重试可能是处理这个问题最简单有效的方法。

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

撰写回答