Подтвердить что ты не робот

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

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

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

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

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

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

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

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

4b9b3361

Ответ 1

Это немного взломать. Необработанный 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()

Ответ 3

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

Ответ 4

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

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

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

Ответ 5

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

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

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

Ответ 6

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

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

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

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

Ответ 7

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

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