33

我的问题如下:

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

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

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

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

4

5 回答 5

29

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

我对 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 上工作。

于 2017-01-24T14:43:56.067 回答
14

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

来源:锁定表

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  
于 2014-02-21T11:10:37.920 回答
12
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 的 f-strings又名文字字符串插值。

于 2019-01-28T13:24:42.887 回答
11

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

您描述的确切场景在此处的 Django 文档中有所提及。根据您的情况,您可以使用以下代码:

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 值。此外,这种方法比锁定表的方法快得多。

于 2020-03-11T17:57:15.957 回答
1

我有同样的问题。F() 解决方案解决了一个不同的问题。它不会获取特定 carmax(order_no)的所有行的值,而是提供一种基于已在字段中为特定行设置的值更新值的方法。sold_carsdealerorder_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()
于 2021-08-19T19:21:06.740 回答