Атомные операции в Django?
Я пытаюсь реализовать (что я думаю) довольно простую модель данных для счетчика:
class VisitorDayTypeCounter(models.Model):
visitType = models.CharField(max_length=60)
visitDate = models.DateField('Visit Date')
counter = models.IntegerField()
Когда кто-то приходит, он будет искать строку, которая соответствует visitType и visitDate; если эта строка не существует, она будет создана с помощью счетчика = 0.
Затем мы увеличиваем счетчик и сохраняем.
Меня беспокоит то, что этот процесс - это всего лишь гонка. Два запроса могли одновременно проверять, существует ли объект, и оба из них могут его создать. Между чтением счетчика и сохранением результата может пройти другой запрос и увеличить его (что приведет к потере счета).
До сих пор я действительно не нашел хорошего способа обойти это, будь то в документации Django или в учебнике (на самом деле, похоже, что у учебника есть условие гонки в части Vote).
Как мне сделать это безопасно?
Ответы
Ответ 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()
Ответ 2
По 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
Ответ 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. Существует промежуточная версия транзакции, или вы можете использовать декораторы вокруг представлений или методов для создания транзакций.