Вопрос

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

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

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

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

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

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

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

Это было полезно?

Решение

Это что-то вроде хака. Необработанный 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()

Другие советы

Начиная с 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 / ы / 1,8 / исй / модель / выражение / # django.db.models.F

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

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

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

visitors = VisitorDayTypeCounter.objects.get(day=curday).for_update()
visitors.counter += 1
visitors.save()

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

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

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

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

<Ол>
  • проверьте, есть ли строка. если это так, принесите его.
  • вставить строку. если нет ошибки, вы в порядке и можете двигаться дальше.
  • если есть ошибка (то есть состояние гонки), повторно извлеките строку. если нет строки, то это настоящая ошибка. В противном случае, ты в порядке.
  • Это так неприятно, но это кажется достаточно быстрым и может охватить большинство ситуаций.

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

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

    Лицензировано под: CC-BY-SA с атрибуция
    Не связан с StackOverflow
    scroll top