如何在Django中避免唯一检查的竞争条件
我有一个简单的模型:
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 个回答
我同意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
约束强制数据库抛出异常的话,情况会好一些。
通常情况下,我们不需要去处理这个问题,因为:
- 在你的情况下,出错的可能性几乎为零;
- 即使出错,影响也很小。
如果因为某种原因你必须确保这个问题不会发生,那就得自己想办法了。
我没有详细分析事件的顺序,但我觉得使用SERIALIZABLE隔离级别并不会真正解决问题,只会在其他地方引发IntegrityError
(或DatabaseError
)。
重写Model._perform_unique_checks
听起来不是个好主意,尽量避免“猴子补丁”(也就是随意修改现有代码),在这里是可以避免的。
至于使用表锁来防止不太可能发生的错误……我对此不太感冒,所以也不推荐这种做法。
这里有个不错的回答,针对类似的问题:https://stackoverflow.com/a/3523439/176186 - 我同意,捕获IntegrityError
并重试可能是处理这个问题最简单有效的方法。
编辑:我发现了这个:Symfony2 - 如何在表单提交后恢复唯一约束错误?,我同意@pid的回答。