Условия гонки в джанго

Вот простой пример представления django с потенциальным состоянием гонки:

# myapp/views.py
from django.contrib.auth.models import User
from my_libs import calculate_points

def add_points(request):
    user = request.user
    user.points += calculate_points(user)
    user.save()

Состояние гонки должно быть довольно очевидным: пользователь может сделать этот запрос дважды, и приложение потенциально может выполнить user = request.user одновременно, в результате чего один из запросов переопределит другой.

Предположим, что функция calculate_points относительно сложна и выполняет вычисления, основанные на всевозможных странных вещах, которые нельзя поместить в одну update и было бы трудно поместить в хранимую процедуру.

Итак, вот мой вопрос: какие механизмы блокировки доступны для django, чтобы справляться с ситуациями, подобными этой?


person Fragsworth    schedule 23.06.2009    source источник
comment
При первом проходе похоже, что вам нужна блокировка уровня базы данных для рассматриваемой строки в этот момент. Я бы проконсультировался с документацией по SQL для вашей базы данных и отправил для этого специальный запрос.   -  person Tom Leys    schedule 23.06.2009
comment
Я бы предпочел решение, не зависящее от базы данных, если это вообще возможно.   -  person Fragsworth    schedule 23.06.2009


Ответы (6)


Django 1.4+ поддерживает select_for_update, в более ранних версиях вы могли выполнять необработанные SQL-запросы, например select ... for update, который в зависимости от базовой БД будет блокировать строку от любых обновлений, вы можете делать с этой строкой все, что хотите, до конца транзакции. например

from django.db import transaction

@transaction.commit_manually()
def add_points(request):
    user = User.objects.select_for_update().get(id=request.user.id)
    # you can go back at this point if something is not right 
    if user.points > 1000:
        # too many points
        return
    user.points += calculate_points(user)
    user.save()
    transaction.commit()
person Anurag Uniyal    schedule 11.06.2012
comment
Похоже, для этой функции уже давно был патч code.djangoproject.com/ticket/2705 — я недавно применил его к Django 1.3.5 (для большого проекта, который сложно мигрировать на 1.4) - person Alex Lokk; 09.08.2013
comment
Мне интересно, как это лучше всего реализовать как метод класса User (чтобы его можно было повторно использовать в других местах, а не только в этом представлении). Проблема для меня в том, что вызывающий код должен по-прежнему выполнять вызов select_for_update(), но я бы хотел, чтобы он был инкапсулирован в методе пользователя. - person Ivan Virabyan; 24.10.2013
comment
@IvanVirabyan либо добавьте определенный метод в класс User, например. get_user, но если вы хотите быть более общим и хотите переопределить все запросы объектов, напишите собственный ModelManager - person Anurag Uniyal; 28.10.2013
comment
Обратите внимание, что Django 1.4 выбирает для обновления блокировку строк из всех таблиц в запросе (SQL позволяет указать подмножество таблиц) — см. groups.google.com/forum/#!topic/django-users/p1qnpz-S9xA. Хорошая статья об этом подходе, написанная до того, как select_for_update() попал в Django 1.4 — coderanger.net/ 2011/01/выбрать для обновления - person RichVel; 15.03.2014

Начиная с Django 1.1 вы можете использовать выражения ORM F() для решения этой конкретной проблемы.

from django.db.models import F

user = request.user
user.points  = F('points') + calculate_points(user)
user.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

person bjunix    schedule 23.12.2009
comment
Выражения F() по-прежнему не позволяют добавлять условие к обновлению. Таким образом, вы можете увеличить количество баллов пользователей, если они все еще активны. - person Jason Webb; 14.01.2011
comment
нет... это не удастся, если у вас есть обновление внутри цикла for! - person NoobEditor; 07.07.2017
comment
Вы также можете использовать F() с обновлением: User.objects.filter(id=user.id).update(points=F('points') + points) - person Mark Mishyn; 02.01.2020

Блокировка базы данных — это то, что нужно. Планируется добавить в Django поддержку «выбрать для обновления» (здесь), но пока самым простым было бы использовать необработанный SQL для ОБНОВЛЕНИЯ объекта пользователя, прежде чем вы начнете вычислять счет.


Пессимистическая блокировка теперь поддерживается ORM Django 1.4, если базовая БД (например, Postgres) поддерживает ее. См. примечания к выпуску Django 1.4a1. .

person zooglash    schedule 23.06.2009

У вас есть много способов однопоточности такого рода.

Одним из стандартных подходов является Обновить сначала. Вы выполняете обновление, которое захватывает монопольную блокировку строки; затем делайте свою работу; и, наконец, зафиксируйте изменение. Чтобы это работало, вам нужно обойти кэширование ORM.

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

  • Ваше веб-приложение может создать очередь запросов на оценку, запустить отдельный процесс, а затем записать запросы на оценку в эту очередь. Спаун можно поместить в urls.py Django, чтобы это происходило при запуске веб-приложения. Или его можно поместить в отдельный manage.py скрипт администратора. Или это можно сделать «по мере необходимости» при попытке первого запроса на оценку.

  • Вы также можете создать отдельный веб-сервер со вкусом WSGI, используя Werkzeug, который принимает запросы WS через urllib2. Если у вас есть один номер порта для этого сервера, запросы ставятся в очередь TCP/IP. Если ваш обработчик WSGI имеет один поток, значит, вы достигли сериализованной однопоточности. Это немного более масштабируемо, поскольку механизм оценки представляет собой запрос WS и может запускаться где угодно.

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

  • Объект Singleton в базе данных. Одна строка в уникальной таблице может быть обновлена ​​с помощью идентификатора сеанса, чтобы захватить контроль; обновить с идентификатором сеанса None, чтобы освободить управление. Важное обновление должно включать фильтр WHERE SESSION_ID IS NONE, чтобы гарантировать, что обновление не будет выполнено, когда блокировка удерживается кем-то другим. Это интересно, потому что по своей сути это не гонка — это одно обновление, а не последовательность SELECT-UPDATE.

  • Стандартный семафор можно использовать вне базы данных. С очередями (как правило) работать легче, чем с низкоуровневым семафором.

person S.Lott    schedule 23.06.2009
comment
Отличный ответ. Каким-то образом доступ к строке базы данных должен быть сериализован, и я думаю, что очереди более масштабируемы, чем блокировки. @Fragsworth: см. этот проект для простой в использовании реализации очередей в Django, использующей RabbitMQ: ask. github.com/celery/introduction.html - person Van Gale; 23.06.2009

Возможно, это слишком упрощает вашу ситуацию, но как насчет замены ссылки на JavaScript? Другими словами, когда пользователь щелкает ссылку или кнопку, оборачивайте запрос в функцию JavaScript, которая немедленно отключает / «закрашивает» ссылку и заменяет текст информацией «Загрузка ...» или «Отправка запроса ...» или что-то в этом роде. похожий. Это сработает для вас?

person Wayne Koorts    schedule 23.06.2009
comment
-1 это все равно не защищает сайт. время от времени пользователи используют другие http-клиенты, а не браузеры. то есть пользователь может использовать wget для получения заданного URL-адреса, тогда отключение URL-адреса с помощью jscript вас не спасет. Jscript следует использовать только для того, чтобы сделать страницу более удобной для пользователя, если вы хотите, но вы не должны использовать его для устранения проблем в приложении на стороне сервера. - person SashaN; 23.06.2009
comment
@SashaN: на плакате не было сказано, что доступ к этому можно будет получить не только через веб-браузер. Мы не можем сразу предположить все другие случаи исключения, такие как wget. Я также добавил к ответу префикс. Возможно, это слишком упрощает вашу ситуацию... чтобы охватить случаи исключения, поскольку это предложение вполне может быть подходящим решением для многих. Подумайте также о будущих зрителях этого вопроса, у которых может быть несколько иной сценарий, в котором этот ответ может быть просто билетом. Я, конечно, не согласен с тем, что это заслуживает неподдерживающего голосования, но я ценю, что вы, по крайней мере, указали причину. - person Wayne Koorts; 24.06.2009
comment
Вы не должны доверять стороне клиента - person Ekevoo; 18.04.2015

Теперь вы должны использовать:

Model.objects.select_for_update().get(foo=bar)
person dingyaguang117    schedule 24.06.2014
comment
Объяснение вашего намерения улучшит ваш ответ. - person Reporter; 24.06.2014