【问题标题】:Django ORM and locking tableDjango ORM 和锁定表
【发布时间】:2013-10-30 14:51:08
【问题描述】:

我的问题如下:

我有一个汽车经销商 A 和一个名为 sold_cars 的数据库表。在出售汽车时,我会在此表中创建条目。

表有一个名为order_no 的整数列。它在经销商销售的汽车中应该是独一无二的。

因此,如果经销商 A 销售汽车 abc,则此列应为 1, 2, 3。我必须使用此列,而不是主键,因为我不想在我的编号中有任何漏洞 - 经销商 A 和 B(可能稍后添加)应该有订单号 1、2、3,而不是 A : 1, 3, 5, 和 B: 2, 4, 6。所以...我为给定的经销商选择最后一个最大的order_no,将其加 1 并保存。

问题是两个人在同一毫秒内从经销商 A 购买了汽车,并且两个订单得到了相同的 order_no。有什么建议吗?我正在考虑在事务块中关闭此进程,并锁定此表直到事务完成,但找不到有关如何执行此操作的任何信息。

【问题讨论】:

    标签: django orm locking


    【解决方案1】:
    from contextlib import contextmanager
    from django.db import transaction
    from django.db.transaction import get_connection
    
    
    @contextmanager
    def lock_table(model):
        with transaction.atomic():
            cursor = get_connection().cursor()
            cursor.execute(f'LOCK TABLE {model._meta.db_table}')
            try:
                yield
            finally:
                cursor.close()
    

    这与@jdepoix 解决方案非常相似,但更简洁一些。

    你可以这样使用它:

    with lock_table(MyModel):
        MyModel.do_something()
    

    请注意,这仅适用于 PostgreSQL 并使用 python 3.6's f-strings 又名文字字符串插值。

    【讨论】:

      【解决方案2】:

      我建议使用F() expression 而不是锁定整个表。如果您的应用被大量使用,锁定表将对性能产生重大影响。

      Django 文档here 中提到了您描述的确切场景。根据您的情况,您可以使用以下代码:

      from django.db.models import F
      
      
      # Populate sold_cars as you normally do..
      # Before saving, use the "F" expression
      sold_cars.order_num =F('order_num') + 1
      sold_cars.save()
      
      # You must do this before referring to order_num:
      sold_cars.refresh_from_db()
      # Now you have the database-assigned order number in sold_cars.order_num
      
      

      请注意,如果您在更新操作期间设置 order_num,请改用以下内容:

      sold_cars.update(order_num=F('order_num')+1)
      sold_cars.refresh_from_db()
      
      

      由于数据库负责更新字段,因此不会有任何竞争条件或重复的 order_num 值。此外,这种方法比锁定表的方法快得多。

      【讨论】:

      • 7年前我最初问这个问题的时候是这样吗?
      • Django 文档不显示 F() 何时首次引入,在 Django 1.8 中也有。但是 7 年很长,你是真正的 Django 先驱!你还记得你使用的 Django 版本吗?
      • 我很确定它接近 1.0 版本,但我不记得具体的版本了。知道 Django 作者是多么周到,很可能这种行为从一开始就存在,但那时我在任何地方都找不到关于该主题的内容。这可能与以下事实有关,我只是一名初级开发人员:P。无论如何,我认为我应该接受你的回答,因为这个问题仍然很受欢迎,其他答案可能会导致人们做一些非常错误的事情。
      • 谢谢!是的,我遇到了你的问题,因为我正在寻找锁定表,所以问题的受欢迎程度很高。
      • 我收到此错误:Failed to insert expression ... F() expressions can only be used to update, not to insert.。我有一张发票表,每张发票都有自己的编号(加一)。最新的发票编号不会存储在任何地方。我希望你建议的功能会找到最大的发票号码,并将最大的发票号码 + 1 添加到新发票中。我想我没有理解这个概念?
      【解决方案3】:

      我遇到了同样的问题。 F() solution 解决了一个不同的问题。它不会为特定汽车dealer 的所有sold_cars 行获取max(order_no),而是提供一种基于已在字段中为特定行设置的值更新order_no 值的方法.

      在这里锁定整个表有点过头了,只锁定特定经销商的行就足够了。

      以下是我最终得到的解决方案。代码假定sold_cars 表引用dealers 表使用sold_cars.dealer 字段。为清楚起见,省略了导入、日志记录和错误处理:

      DEFAULT_ORDER_NO = 0
      
      def save_sold_car(sold_car, dealer):
          # update sold_car instance as you please
          with transaction.atomic():
              # to successfully use locks the processes must query for row ranges that
              # intersect. If no common rows are present, no locks will be set.
              # We save the sold_car entry without an order_no to create at least one row
              # that can be locked. If order_no assignment fails later at some point,
              # the transaction will be rolled back and the 'incomplete' sold_car entry
              # will be removed
              sold_car.save()
              # each process adds its own sold_car entry. Concurrently getting sold_cars
              # by their dealer may result in row ranges which don't intersect.
              # For example process A saves sold_car 'a1' the same moment process B saves
              # its 'b1' sold_car. Then both these processes get sold_cars for the same
              # dealer. Process A gets single 'a1' row, while process B gets
              # single 'b1' row.
              # Since all the sold_cars here belong to the same dealer, adding the
              # related dealer's row to each range with 'select_related' will ensure
              # having at least one common row to acquire the lock on.
              dealer_sold_cars = (SoldCar.objects.select_related('dealer')
                                                 .select_for_update()
                                                 .filter(dealer=dealer))
              # django queries are lazy, make sure to explicitly evaluate them
              # to acquire the locks
              len(dealer_sold_cars)
              max_order_no = (dealer_sold_cars.aggregate(Max('order_no'))
                                              .get('order_no__max') or DEFAULT_ORDER_NO)
              sold_car.order_no = max_order_no + 1
              sold_car.save()
      

      【讨论】:

        【解决方案4】:

        我知道这个问题有点老了,但我也遇到了同样的问题,想分享我的经验。

        我对 st0nes 的回答不太满意,因为(至少对于 postgres 而言)LOCK TABLE 语句只能在事务中发出。尽管在 Django 中,几乎所有事情都发生在事务中,但 LockingManager 并不能确保您实际上在事务中,至少在我的理解中是这样。此外,我不想完全更改模型Manager 只是为了能够将其锁定在一个位置,因此我更想寻找一些类似于with transaction.atomic(): 的东西,但也可以锁定给定的模型。

        所以我想出了这个:

        from django.conf import settings
        from django.db import DEFAULT_DB_ALIAS
        from django.db.transaction import Atomic, get_connection
        
        
        class LockedAtomicTransaction(Atomic):
            """
            Does a atomic transaction, but also locks the entire table for any transactions, for the duration of this
            transaction. Although this is the only way to avoid concurrency issues in certain situations, it should be used with
            caution, since it has impacts on performance, for obvious reasons...
            """
            def __init__(self, model, using=None, savepoint=None):
                if using is None:
                    using = DEFAULT_DB_ALIAS
                super().__init__(using, savepoint)
                self.model = model
        
            def __enter__(self):
                super(LockedAtomicTransaction, self).__enter__()
        
                # Make sure not to lock, when sqlite is used, or you'll run into problems while running tests!!!
                if settings.DATABASES[self.using]['ENGINE'] != 'django.db.backends.sqlite3':
                    cursor = None
                    try:
                        cursor = get_connection(self.using).cursor()
                        cursor.execute(
                            'LOCK TABLE {db_table_name}'.format(db_table_name=self.model._meta.db_table)
                        )
                    finally:
                        if cursor and not cursor.closed:
                            cursor.close()
        

        所以如果我现在想锁定模型ModelToLock,可以这样使用:

        with LockedAtomicTransaction(ModelToLock):
            # do whatever you want to do
            ModelToLock.objects.create()
        

        编辑:请注意,我只使用 postgres 对此进行了测试。但据我了解,它也应该像那样在 mysql 上工作。

        【讨论】:

        • Mysq 和 Django 2.2: AttributeError, 'Cursor' has no attribute 'closed'
        • 请注意,这不起作用,因为 Django 添加了 durable 字段。参考devsnd答案。
        【解决方案5】:

        假设您使用的是 MySQL,我认为此代码 sn-p 满足您的需求。如果没有,您可能需要稍微调整语法,但这个想法应该仍然有效。

        来源:Locking tables

        class LockingManager(models.Manager):
            """ Add lock/unlock functionality to manager.
        
            Example::
        
                class Job(models.Model):
        
                    manager = LockingManager()
        
                    counter = models.IntegerField(null=True, default=0)
        
                    @staticmethod
                    def do_atomic_update(job_id)
                        ''' Updates job integer, keeping it below 5 '''
                        try:
                            # Ensure only one HTTP request can do this update at once.
                            Job.objects.lock()
        
                            job = Job.object.get(id=job_id)
                            # If we don't lock the tables two simultanous
                            # requests might both increase the counter
                            # going over 5
                            if job.counter < 5:
                                job.counter += 1                                        
                                job.save()
        
                        finally:
                            Job.objects.unlock()
        
        
            """    
        
            def lock(self):
                """ Lock table. 
        
                Locks the object model table so that atomic update is possible.
                Simulatenous database access request pend until the lock is unlock()'ed.
        
                Note: If you need to lock multiple tables, you need to do lock them
                all in one SQL clause and this function is not enough. To avoid
                dead lock, all tables must be locked in the same order.
        
                See http://dev.mysql.com/doc/refman/5.0/en/lock-tables.html
                """
                cursor = connection.cursor()
                table = self.model._meta.db_table
                logger.debug("Locking table %s" % table)
                cursor.execute("LOCK TABLES %s WRITE" % table)
                row = cursor.fetchone()
                return row
        
            def unlock(self):
                """ Unlock the table. """
                cursor = connection.cursor()
                table = self.model._meta.db_table
                cursor.execute("UNLOCK TABLES")
                row = cursor.fetchone()
                return row  
        

        【讨论】:

        • cursor.fetchone() 在这里做什么?
        猜你喜欢
        • 2013-01-25
        • 1970-01-01
        • 2015-03-27
        • 2011-06-07
        • 1970-01-01
        • 2021-10-01
        • 1970-01-01
        • 2020-04-16
        • 2016-12-09
        相关资源
        最近更新 更多