Расовые условия в Джанго - PullRequest
34 голосов
/ 23 июня 2009

Вот простой пример представления 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, чтобы справиться с подобными ситуациями?

Ответы [ 6 ]

44 голосов
/ 12 июня 2012

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()
17 голосов
/ 24 декабря 2009

Начиная с 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

8 голосов
/ 23 июня 2009

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


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

6 голосов
/ 23 июня 2009

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

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

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

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

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

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

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

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

1 голос
/ 23 июня 2009

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

0 голосов
/ 24 июня 2014

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

Model.objects.select_for_update().get(foo=bar)
...