Атомные операции в Джанго?
-
07-07-2019 - |
Вопрос
Я пытаюсь реализовать (как мне кажется,) довольно простую модель данных для счетчика:
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 / ы / 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. Существует промежуточное программное обеспечение транзакции, или вы можете использовать декораторы вокруг представлений или методов для создания транзакций.