Можно ли использовать 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 поток одновременно будет иметь доступ к этому блоку кода.
Мои вопросы следующие:
- Is my approach doing what I think it's doing?
- Is this a clean approach? if not, how can this be achieved without complicating the codebase and going through a major refactor?