Атомарные операции в Django?

Я пытаюсь реализовать (как мне кажется) довольно простую модель данных для счетчика:

class VisitorDayTypeCounter(models.Model):
    visitType = models.CharField(max_length=60)
    visitDate = models.DateField('Visit Date')
    counter = models.IntegerField()

Когда кто-то приходит, он будет искать строку, которая соответствует visitType и visitDate; если эта строка не существует, она будет создана с counter = 0.

Затем увеличиваем счетчик и сохраняем.

Меня беспокоит то, что этот процесс - сплошная гонка. Два запроса могут одновременно проверять наличие сущности, и оба они могут ее создать. Между чтением счетчика и сохранением результата может поступить другой запрос и увеличить его (что приведет к потере счетчика).

Пока что я не нашел хорошего способа обойти это ни в документации Django, ни в учебнике (на самом деле, похоже, что в учебнике есть условие гонки в части голосования).

Как мне это сделать безопасно?


person Tony Arkles    schedule 11.11.2008    source источник


Ответы (7)


Это что-то вроде хака. Необработанный SQL сделает ваш код менее переносимым, но избавится от состояния гонки на приращении счетчика. Теоретически это должно увеличивать счетчик каждый раз, когда вы выполняете запрос. Я не тестировал это, поэтому вы должны убедиться, что список правильно интерполируется в запросе.

class VisitorDayTypeCounterManager(models.Manager):
    def get_query_set(self):
        qs = super(VisitorDayTypeCounterManager, self).get_query_set()

        from django.db import connection
        cursor = connection.cursor()

        pk_list = qs.values_list('id', flat=True)
        cursor.execute('UPDATE table_name SET counter = counter + 1 WHERE id IN %s', [pk_list])

        return qs

class VisitorDayTypeCounter(models.Model):
    ...

    objects = VisitorDayTypeCounterManager()
person iconoplast    schedule 12.11.2008
comment
Неужели все еще возможно, что база данных будет выполнять этот запрос по двум отдельным соединениям одновременно и по-прежнему иметь (гораздо более низкую вероятность) состояние гонки? Все зависит от скрытых транзакций вокруг этого запроса, подразумеваемых уровнем соединения, которые делают операцию атомарной. - person Tom Leys; 12.11.2008
comment
Если вы посмотрите лейтмотив DjangoCon «Почему я ненавижу Django», то этот тип запроса задается как правильный, без условий гонки способ выполнить приращение в SQL (загвоздка в том, что ORM Django не может сделать это за вас). - person iconoplast; 12.11.2008
comment
Я посмотрю ваши слайды ... вы в значительной степени подтвердили мое подозрение, что ORM не сделает этого самостоятельно. Спасибо за помощь! - person Tony Arkles; 12.11.2008
comment
Если вы посмотрите на Django Ticket 7210 - Добавлена ​​поддержка выражений для QuerySet.update, может показаться, что скоро появится помощь, чтобы сделать это переносимым способом с помощью ORM ... - person Ber; 14.11.2008
comment
Этот ответ мог быть верным когда-то, но он давно устарел. Начиная с Django 1.1, это напрямую поддерживается ORM Django. - person bjunix; 24.08.2014

Начиная с Django 1.1, вы можете использовать выражения ORM F ().

from django.db.models import F
product = Product.objects.get(name='Venezuelan Beaver Cheese')
product.number_sold = F('number_sold') + 1
product.save()

Подробнее см. Документацию:

https://docs.djangoproject.com/en/1.8/ref/models/instances/#updating-attributes-based-on-existing-fields

https://docs.djangoproject.com/en/1.8/ref/models/expressions/#django.db.models.F

person bjunix    schedule 23.12.2009
comment
Прохладный! Это потрясающий подход. Если бы только это было, когда я работал над этим проектом. - person Tony Arkles; 25.12.2009
comment
Для современных установок Django это правильный ответ, и он должен быть отражен как таковой в OP. - person claymation; 23.05.2012

Если вы действительно хотите, чтобы счетчик был точным, вы можете использовать транзакцию, но требуемый объем параллелизма действительно затянет ваше приложение и базу данных при любой значительной нагрузке. Вместо этого подумайте о том, чтобы перейти к более стилю обмена сообщениями и просто продолжайте сбрасывать записи счетчика в таблицу для каждого посещения, где вы хотите увеличить счетчик. Затем, когда вы хотите, чтобы общее количество посещений было подсчитано в таблице посещений. У вас также может быть фоновый процесс, который запускается любое количество раз в день, который суммирует посещения, а затем сохраняет это в родительской таблице. Чтобы сэкономить место, он также удалит все записи из таблицы дочерних посещений, которые он суммировал. Вы значительно сократите свои затраты на параллелизм, если у вас нет нескольких агентов, конкурирующих за одни и те же ресурсы (счетчик).

person Sam Corder    schedule 11.11.2008
comment
Привет, хороший звонок! Я в основном выполнял работу с App Engine, и я зациклился на том, что транзакции действуют только с одной записью, а выполнение агрегатных функций очень дорого. Это действительно простой способ решить проблему. Спасибо! - person Tony Arkles; 11.11.2008
comment
Я полагаю, это действительно зависит от того, будет ли процесс тяжелым для чтения или для записи. Счетчики будут считываться гораздо чаще, чем они будут увеличиваться в моей системе, поэтому для указанной проблемы это может быть не лучший план. Тем не менее, он решает другие проблемы, которые у меня были, поэтому спасибо! - person Tony Arkles; 11.11.2008
comment
В зависимости от того, насколько устаревшими могут быть подсчеты, у вас может быть фоновый процесс, суммирующий их время от времени. Тогда вы бы не выполняли агрегацию по запросу. - person Sam Corder; 11.11.2008
comment
Интересно знать, что этот метод ведения журнала похож на то, как транзакции делаются безопасными, атомарными и восстанавливаемыми внутри многих баз данных (см., Например, подробности реализации в руководстве Postgresql). - person Tom Leys; 12.11.2008

Вы можете использовать патч из http://code.djangoproject.com/ticket/2705 для поддержки блокировка уровня базы данных.

С патчем этот код будет атомарным:

visitors = VisitorDayTypeCounter.objects.get(day=curday).for_update()
visitors.counter += 1
visitors.save()
person kmmbvnr    schedule 16.12.2008
comment
Это круто. Я не заметил этого, когда впервые задал вопрос (3 года назад!) - person Tony Arkles; 17.09.2011

Два предложения:

Добавьте unique_toght в свою модель и оберните создание в обработчик исключений, чтобы перехватить дубликаты:

class VisitorDayTypeCounter(models.Model):
    visitType = models.CharField(max_length=60)
    visitDate = models.DateField('Visit Date')
    counter = models.IntegerField()
    class Meta:
        unique_together = (('visitType', 'visitDate'))

После этого у вас может остаться незначительное состояние гонки при обновлении счетчика. Если у вас достаточно трафика, чтобы беспокоиться об этом, я бы посоветовал изучить транзакции для более детального управления базой данных. Я не думаю, что ORM имеет прямую поддержку блокировки / синхронизации. Документация по транзакции доступна здесь.

person Daniel Naab    schedule 11.11.2008
comment
Уникальность_всего определенно заставляет меня чувствовать себя немного комфортнее. Скорее всего, на нем никогда не будет достаточно трафика, чтобы заставить гонку ударить, но, поскольку я одновременно изучаю Django, я решил, что хочу сделать это правильно. Спасибо за помощь! - person Tony Arkles; 11.11.2008
comment
Да, я тебя слышу. Может быть, кто-то еще знает о функции ORM для обработки этого или может выяснить, безопасны ли некоторые из встроенных модулей для этого сценария. - person Daniel Naab; 11.11.2008

Почему бы не использовать базу данных в качестве уровня параллелизма? Добавьте первичный ключ или уникальное ограничение таблицы в visitType и visitDate. Если я не ошибаюсь, django точно не поддерживает это в своем классе модели базы данных, или, по крайней мере, я не видел примера.

После того, как вы добавили ограничение / ключ в таблицу, все, что вам нужно сделать, это:

  1. проверьте, есть ли строка. если это так, принесите его.
  2. вставить строку. если ошибок нет, все в порядке и можно двигаться дальше.
  3. если есть ошибка (например, состояние гонки), повторно выберите строку. если строки нет, то это настоящая ошибка. В противном случае все в порядке.

Это неприятно делать таким образом, но кажется, что это достаточно быстро и подходит для большинства ситуаций.

person Roopinder    schedule 11.11.2008
comment
Он не обрабатывает случай, когда два человека одновременно обновляют счетчик. - person Tony Arkles; 11.11.2008

Вы должны использовать транзакции базы данных, чтобы избежать такого состояния гонки. Транзакция позволяет вам выполнять всю операцию по созданию, чтению, увеличению и сохранению счетчика по принципу «все или ничего». Если что-то пойдет не так, все откатится, и вы сможете попробовать еще раз.

Ознакомьтесь с документацией по Django. промежуточное ПО для транзакций, или вы можете использовать декораторы вокруг представлений или методов для создания транзакций.

person Ber    schedule 11.11.2008
comment
Я согласен с тем, что здесь транзакции кажутся ответом, но неясно, действительно ли эта функциональность решит проблему приращения - SELECT для получения строки все равно будет успешным, а UPDATE для изменения значения счетчика все равно будет успешным. Если я ошибаюсь, пример был бы классным. - person Tony Arkles; 11.11.2008
comment
Вам нужно будет заблокировать таблицу во время выбора, чтобы сделать это таким образом, и, как упоминал Сэм, это снизит вашу производительность. Это лучший способ, если вы не часто задеваете счетчик. - person Tom Leys; 12.11.2008