Django:交易和select_for_update()

Ali*_*uis 5 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)])
Run Code Online (Sandbox Code Playgroud)

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']
Run Code Online (Sandbox Code Playgroud)

现在不同的问题:

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

Kev*_*nry 6

在查询集select_for_update()上做EventRegistration不是要走的路。这会锁定指定的行,但大概您试图防止的冲突涉及创建新的 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
Run Code Online (Sandbox Code Playgroud)

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

  • @Aliquis:您只能锁定实际存在的行。如果该“事件”没有现有的“事件注册”,并且有两个请求发生冲突,您的方法不会阻止它们。 (3认同)
  • @A.Khaled:一次只有一个线程可以获取锁(对于给定的“Event”),因此两个线程不能同时到达“if”条件。获取锁时会阻塞。这段代码基本上是做你所要求的,但它不是一个“读锁”,它是一个 [`SELECT FOR UPDATE`](https://www.postgresql.org/docs/current/explicit-locking.html #LOCKING-ROWS)锁定。 (3认同)