【问题标题】:Can Django select_for_update be used to acquire a read lock?Django select_for_update 可以用来获取读锁吗?
【发布时间】:2021-09-14 07:01:40
【问题描述】:

我正在从事一个类似于电子商务的项目,其中我的模型代表存储中有一定数量的产品,用户可以购买一定数量的产品,只要它不超过存储的数量.当服务器收到购买同一产品的多个请求时,我想避免发生竞争情况。

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

我的问题是:

  1. 我的方法是否符合我的想法?
  2. 这是一种干净的方法吗?如果没有,如何在不使代码库复杂化并进行重大重构的情况下实现这一目标?

【问题讨论】:

    标签: python django concurrency transactions locking


    【解决方案1】:
    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
      

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

    【讨论】:

      猜你喜欢
      • 2021-12-20
      • 1970-01-01
      • 2020-11-06
      • 2020-08-29
      • 2013-06-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-08-05
      相关资源
      最近更新 更多