Django select_for_update 可以用来获取读锁吗?

Can Django select_for_update be used to acquire a read lock?

我正在做一个类似于电子商务的项目,在这个项目中,我有一些模型来表示存储中有一定数量的产品,用户可以购买一定数量的产品,只要它不超过存储量。我想避免在服务器收到多个购买同一产品的请求时发生竞争条件。

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

我想限制对这个代码块的并发访问,理想情况下我想在 if 条件下获得一个 read 锁访问,这样如果多个线程尝试查看是否可用数量大于购买数量,其中一个线程必须等到交易完成,然后才会评估读取请求,所以我想利用 Django 的 select_for_updateversion 像这样的字段:

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如果多个线程到达版本修改行,只有第一个会评估,其余的必须等到整个事务完成,因此获取第一个行的读锁如果条件。换句话说,一次只有 1 个线程可以访问此代码块。

我的问题是:

  1. 我的方法是否符合我的预期?
  2. 这是一个干净的方法吗?如果不是,如何在不使代码库复杂化和进行重大重构的情况下实现这一点?
  1. 是的,你的做法非常正确,它会在第一次查询时获取锁,其余的必须等到事务完成。

  2. 如果除了获取锁之外没有使用版本字段,您可以通过使用 list() 强制评估 QuerySet 来强制获取锁。请参阅下面的代码。

    @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
    

在这里,您通过评估 docs 中提到的 QuerySet 来强制获取锁。