Запретить Django несколько раз сохранять один и тот же объект в базе данных при одновременных запросах
Для нашей платформы блогов у нас есть модель "Article", которая содержит "обновленное" поле datetime:
class Article(models.Model):
updated = models.DateTimeField(null=True, blank=True)
...
Когда какой-либо посетитель открывает статью впервые за 24 часа, мы выполняем некоторые трудоемкие вычисления для различных полей модели, а затем сохраняем модель в базе данных. При этом мы также обновляем наше "обновленное" поле до текущего datetime.now().
if (datetime.now() - article.updated).days > 1:
# do some time consuming calculations
article.updated = datetime.now()
article.save()
Когда статья запрашивается более или менее одновременно, трудоемкие операции по первому запросу еще не завершены, что приводит к повторному запуску операции один раз в день для того же объекта (article.updated по-прежнему имеет старое значение). Может ли это помочь дополнительно вызвать article.save() прямо перед началом вычислений? Или эти данные перенесены с сохранения в базу данных до завершения запроса?
3 ответа
Используйте набор запросов select_for_update, представленный в Django 1.4, который выполняет блокировку на уровне строк в базе данных. Все совпадающие записи будут заблокированы до конца блока транзакции, что означает, что другие транзакции не смогут изменить или получить блокировки на них. Есть несколько ошибок, специфичных для бэкэнда базы данных, поэтому обязательно прочитайте и протестируйте его, прежде чем полностью полагаться на него.
Некоторые другие способы сделать это независимо от реализации, настроив ваши модели, чтобы иметь locked
логический атрибут. Не очень аккуратное, но работоспособное решение. Посмотрите, как проще всего заблокировать объект в Django
Укороченная версия:
@transaction.commit_on_success
def update_article( article_id ):
article = Article.objects.select_for_update().get( pk = article_id )
if (datetime.now() - article.updated).days > 1:
# do some time consuming calculations
article.updated = datetime.now()
article.save()
select_for_update()
блокирует строку базы данных (статья с идентификатором article_ID). Строка разблокируется в конце транзакции, которая находится в конце функции, так как update_article()
обернут @transaction.commit_on_success
,
Ps: доступно с Django 1.4
Некоторые предложения:
- Было бы лучше переместить трудоемкие вычисления из цикла запрос-ответ в фоновый режим. Здесь можно использовать очереди сообщений (например, популярный сельдерей). Я думаю, что это лучшее решение, но оно может потребовать некоторого дополнительного администрирования, которое может быть излишним для простых задач;
- Если вы используете кеш, вы можете установить флаг, что объект заблокирован. Если кеш является общим для разных интерпретаторов (например, memcached), он будет работать, даже если у вас есть много интерпретаторов Python, которые запускают ваше приложение;
- Вы можете запланировать процедуру обновления (используя cron и пользовательскую команду управления Django), чтобы обновить все объекты, которые были обновлены>24 часа назад. Это будет работать, если у вас нет большого количества объектов и значительного времени обработки.