Можно ли использовать Django select_for_update для получения блокировки на чтение?

Я работаю над проектом, похожим на электронную коммерцию, в котором у меня есть модели, представляющие продукт с определенным количеством в хранилище, и пользователи могут купить некоторое количество продукта, пока оно не превышает хранимое количество. Я хочу избежать возникновения состояния гонки, когда сервер получает несколько запросов на покупку одного и того же продукта.

class Product(models.Model):
   amount_in_storage = models.PositiveIntegerField() # for design reasons, this amount is unchangeable, it must be only evaluated once during initialization like constants in C++

   @property
   def amount_available_for_purchase(self):
       return self.amount_in_storage - Purchase.objects.filter(product=self.id).aggregate(models.Sum('amount'))["sum__amount"]
class Purchase(models.Model):
   product = models.ForeignKey(Product, ...)
   amount = models.PositiveIntegerField()
   payment_method = ...

Предположим, что это блок кода, который отвечает за создание покупки.

@atomic_transaction
def func(product_id, amount_to_purchase):
   product = Product.objects.get(...)
   if product.amount_available_for_purchase > amount_to_purchase:
     # do payment related stuff
     Purchase.objects.create(product=product, amount=amount_to_purchase)
     # do more stuff

Я хотел бы ограничить одновременный доступ к этому блоку кода, в идеале я хотел бы получить доступ к блокировке read в условии if, так что если несколько потоков попытаются проверить, больше ли доступная сумма, чем сумма для покупки, один из потоков должен будет подождать, пока транзакция не будет выполнена, а затем будет оценен запрос на чтение, поэтому я думал использовать select_for_update в Django и поле version следующим образом:

class Product(models.Model):
   amount_in_storage = models.PositiveIntegerField()
   version = models.PositiveIntegerField() # we use this field just to write to it, no reading will take place

   @property
   def amount_available_for_purchase(self):
       return self.amount_in_storage - Purchase.objects.filter(product=self.id).aggregate(models.Sum('amount'))["sum__amount"]

И я использую это поле для получения блокировки следующим образом:


@atomic_transaction
def func(product_id, amount_to_purchase):
   product = Product.objects.select_for_update.get(...)

   # acquiring a lock
   product.version += 1 
   product.save()

   if product.amount_available_for_purchase > amount_to_purchase:
     # do payment related stuff
     Purchase.objects.create(product=product, amount=amount_to_purchase)
     # do more stuff

Используя select_for_update, если несколько потоков достигнут строки модификации версии, только первый из них выполнит оценку, а остальные будут вынуждены ждать завершения всей транзакции, таким образом, приобретая блокировку чтения для строки в условии if. Другими словами, только 1 поток одновременно будет иметь доступ к этому блоку кода.

Мои вопросы следующие:

  1. Is my approach doing what I think it's doing?
  2. Is this a clean approach? if not, how can this be achieved without complicating the codebase and going through a major refactor?
Вернуться на верх