Django:Transaction和select_for_update()

时间:2018-09-22 08:40:40

标签: python django

在我的Django应用程序中,我有以下两个模型:

class Event(models.Model):
    capacity = models.PositiveSmallIntegerField()

    def get_number_of_registered_tickets():
        return EventRegistration.objects.filter(event__exact=self).aggregate(total=Coalesce(Sum('number_tickets'), 0))['total']


class EventRegistration(models.Model):
    time = models.DateTimeField(auto_now_add=True)
    event = models.ForeignKey(Event, on_delete=models.CASCADE)
    number_tickets = models.PositiveSmallIntegerField(validators=[MinValueValidator(1)])

我在应用程序的多个位置都需要使用方法get_number_of_registered_tickets()(例如,模板渲染)。因此,我认为将其放入模型中也是有意义的,因为它与之相关,而且我经常听到拥有“胖模型和轻量级视图”是很好的。

我现在的问题是: 为了防止两个人要同时注册事件 ,我必须使用锁定。范例:假设还有一张车票要注册。现在,向人们展示在我的网站上,同时单击“注册”。在不幸的情况下,这两个请求都可能有效,现在我的注册量超出了容量。

我是Django的新手,但阅读文档后,我认为应该select_for_update()是解决方案,我在这里吗(我使用PostgreSQL,因此应该受支持)?

但是,文档还说,使用select_for_update()仅在交易中有效。

  

在自动提交模式下使用select_for_update()评估查询集   支持SELECT ... FOR UPDATE的后端是一个   TransactionManagementError错误,因为行未锁定   这种情况。如果允许,这将促进数据损坏,并且可能   很容易由调用期望在   一笔交易。

我现在的想法是更改模型方法get_number_of_registered_tickets()并添加 select_for_update()

def get_number_of_registered_tickets():
        return EventRegistration.objects.select_for_update().filter(event__exact=self).aggregate(total=Coalesce(Sum('number_tickets'), 0))['total']

现在有不同的问题:

  1. 使用select_for_update()是解决我的问题的正确方法吗?
  2. 这是否意味着我现在无法在不同的视图/模板中使用方法get_number_of_registered_tickets(),因为它似乎只能在事务中使用?我是否必须在这里违反DRY并将带有select_for_update()的查询复制并粘贴到代码中的其他位置?
  3. 我在本地进行了测试,并且在自动提交模式(不使用任何事务)下,Django不会提高TransactionManagementError。可能是什么原因,或者我误会了什么?

3 个答案:

答案 0 :(得分:1)

select_for_update()查询集执行EventRegistration并不是路要走。这将锁定指定的行,但是可能您要防止的冲突涉及创建 new EventRegistrations。您的锁不会阻止这种情况。

相反,您可以获得Event上的锁。像这样:

class Event(models.Model):
    ...
    @transaction.atomic
    def reserve_tickets(self, number_tickets):
        list(Event.objects.filter(id=self.id).select_for_update())  # force evaluation
        if self.get_number_of_registered_tickets() + number_tickets <= self.capacity:
            # create EventRegistration
        else:
            # handle error

请注意,这使用transaction.atomic装饰器来确保您在事务内部运行。

答案 1 :(得分:0)

select_for_update是使用Django实现的数据库功能选项。每当您编写操作进行一些更新时,数据库(在您的情况下为POSTGRES)都会照顾遵守这些ACID属性的可靠事务。

对我来说,您的方法似乎正确。最后一个问题的答案是使用time.sleep延迟进行测试。

您可以执行选择操作,然后在下一行放入time.sleep(10),此时会在api上进行另一笔交易。您将能够找到 TransactionManagementError

答案 2 :(得分:0)

请注意,在多个数据库环境中,您必须在同一数据库上具有atomic和select_for_update,否则它将无法正常工作

with transaction.atomic(using='dbwrite'):
     Model.objects.using('dbwrite').select_for_update()....