Атомные операции в Джанго? - PullRequest
24 голосов
/ 11 ноября 2008

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

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

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

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

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

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

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

Ответы [ 7 ]

29 голосов
/ 24 декабря 2009

Начиная с 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

12 голосов
/ 11 ноября 2008

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

6 голосов
/ 16 декабря 2008

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

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

visitors = VisitorDayTypeCounter.objects.get(day=curday).for_update()
visitors.counter += 1
visitors.save()
5 голосов
/ 11 ноября 2008

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

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

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 имеет прямую поддержку для блокировки / синхронизации. Документация по сделке доступна здесь .

1 голос
/ 12 ноября 2008

Это что-то вроде хака. Необработанный 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()
1 голос
/ 11 ноября 2008

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

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

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

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

0 голосов
/ 11 ноября 2008

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

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

...