Условия гонки в django
-
06-07-2019 - |
Вопрос
Вот простой пример представления 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 для решения ситуаций, подобных этой?
Решение
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()
Другие советы
Начиная с 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 / ы / 1,8 / исй / модель / выражение / # django.db.models.F
Блокировка базы данных - это то, что нужно. Есть планы добавить " выбрать для обновления " поддержка Django ( здесь ), но на данный момент самым простым будет использование необработанного SQL для UPDATE пользовательский объект, прежде чем начать рассчитывать балл.
<Ч>Пессимистическая блокировка теперь поддерживается ORM в Django 1.4, когда ее поддерживает базовая БД (например, Postgres). См. заметки о выпуске Django 1.4a1. . р>
У вас есть много способов сделать что-то подобное в одном потоке.
Одним из стандартных подходов является Сначала обновите.Вы выполняете обновление, которое захватывает эксклюзивную блокировку строки;тогда делай свою работу;и, наконец, зафиксируйте изменение.Чтобы это сработало, вам нужно обойти кэширование ORM.
Другой стандартный подход заключается в наличии отдельного однопоточного сервера приложений, который изолирует веб-транзакции от сложных вычислений.
Ваше веб-приложение может создать очередь запросов на оценку, запустить отдельный процесс, а затем записать запросы на оценку в эту очередь.Порождение может быть помещено в Django
urls.py
так что это происходит при запуске веб-приложения.Или его можно поместить в отдельныйmanage.py
скрипт администратора.Или это может быть сделано "по мере необходимости" при попытке первого запроса оценки.Вы также можете создать отдельный веб-сервер с поддержкой WSGI, используя Werkzeug, который принимает запросы WS через urllib2.Если у вас есть один номер порта для этого сервера, запросы ставятся в очередь по протоколу TCP /IP.Если ваш обработчик WSGI имеет один поток, значит, вы достигли сериализованной однопоточности.Это немного более масштабируемо, поскольку механизм подсчета очков является запросом WS и может быть запущен где угодно.
Еще один подход заключается в том, чтобы иметь какой-то другой ресурс, который необходимо приобрести и удерживать для выполнения расчета.
Одноэлементный объект в базе данных.Одна строка в уникальной таблице может быть обновлена идентификатором сеанса для захвата контроля;обновить с идентификатором сеанса
None
чтобы ослабить контроль.Существенное обновление должно включать в себяWHERE SESSION_ID IS NONE
фильтруйте, чтобы убедиться, что обновление завершается неудачно, когда блокировка удерживается кем-то другим.Это интересно, потому что по своей сути не зависит от гонки - это одиночное обновление, а не последовательность ВЫБОРА ОБНОВЛЕНИЙ.Семафор садового типа может использоваться вне базы данных.С очередями (как правило) работать проще, чем с низкоуровневым семафором.
Возможно, это упрощает вашу ситуацию, но как насчет замены ссылки JavaScript? Другими словами, когда пользователь щелкает ссылку или кнопку, оборачивает запрос в функцию JavaScript, которая немедленно отключает / " отключает " ссылка и заменяет текст на "Загрузка ..." или " Отправка запроса ... " информация или что-то подобное. Будет ли это работать для вас?
Теперь вы должны использовать:
Model.objects.select_for_update().get(foo=bar)