简体   繁体   中英

Can Django select_for_update be used to acquire a read lock?

I'm working on a project similar to ecommerce in which I have models that represent a product with a certain amount in the storage, and users can buy some quantity of the product as long as it doesn't exceed the stored amount. I want to avoid a race condition taking place when the server receives multiple requests to buy the same product.

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 = ...

Let's assume that this is the block of code that's responsible for creating a purchase.

@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

I would like to restrict concurrent access to this block of code, Ideally I would like to obtain a read lock access at the if condition, so that if multiple threads try to see if the amount available is greater than the amount to purchase, one of the threads will have to wait until the transaction is done, and then the read request will be evaluated, so I thought leveraging Django's select_for_update and a version field like so:

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"]

And I use this field to obtain a lock like so:


@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

Using select_for_update if multiple threads reach the version modification line, only the first will evaluate, and the rest will have to wait until the whole transaction is completed, hence acquiring a read lock for the line at the if condition. In other words, only 1 thread at a time will have access to this block of code.

My questions are:

  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?
  1. Yes, your approach is pretty much right and it will acquire the lock for the first query and the rest will have to wait until the transaction is complete.

  2. If you are not using the version field for anything other than acquiring the lock, you can forcefully acquire a lock by forcefully evaluating the QuerySet using a list() . See the code below.

     @atomic_transaction def func(product_id, amount_to_purchase): # Forcefully acquiring a lock product = list(Product.objects.select_for_update.filter(...))[0] 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

Here you are acquiring the lock forcefully by evaluating the QuerySet like mentioned here in the docs .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM