Как мне справиться с этим состоянием гонки в django?

Этот код должен получить или создать объект и при необходимости обновить его. Код находится в производственном использовании на веб-сайте.

В некоторых случаях - когда база данных занята - генерируется исключение «DoesNotExist: соответствующий запрос MyObj не существует».

# Model:
class MyObj(models.Model):
    thing = models.ForeignKey(Thing)
    owner = models.ForeignKey(User)
    state = models.BooleanField()
    class Meta:
        unique_together = (('thing', 'owner'),)

# Update or create myobj
@transaction.commit_on_success
def create_or_update_myobj(owner, thing, state)
    try:
        myobj, created = MyObj.objects.get_or_create(owner=user,thing=thing)

    except IntegrityError:
        myobj = MyObj.objects.get(owner=user,thing=thing)
        # Will sometimes throw "DoesNotExist: MyObj matching query does not exist"

    myobj.state = state
    myobj.save()

Я использую базу данных mysql innodb на ubuntu.

Как мне безопасно решить эту проблему?


person Hobhouse    schedule 10.02.2010    source источник


Ответы (4)


Это могло быть ответвлением той же проблемы, что и здесь:

Почему этот цикл отображать обновленное количество объектов каждые пять секунд?

Обычно get_or_create может завершиться ошибкой - если вы посмотрите на его источник, вы увидите, что это: get, if-problem: save + some_trickery, if-still-problem: получить еще раз, if-still-problem: сдаться и поднять .

Это означает, что если есть два одновременных потока (или процесса), выполняющих create_or_update_myobj, оба пытаются получить_или_создать один и тот же объект, тогда:

  • первый поток пытается получить его - но он еще не существует,
  • Итак, поток пытается создать его, но до того, как объект будет создан ...
  • ... второй поток пытается получить его - и это явно не удается
  • теперь из-за значения по умолчанию AUTOCOMMIT = OFF для подключения к базе данных MySQLdb и сериализуемого уровня REPEATABLE READ оба потока заморозили свои представления таблицы MyObj.
  • впоследствии первый поток создает свой объект и изящно возвращает его, но ...
  • ... второй поток не может ничего создать, поскольку это нарушит ограничение unique
  • что забавно, последующий get во втором потоке не видит объект, созданный в первом потоке, из-за зависшего представления таблицы MyObj

Итак, если вы хотите безопасно get_or_create что-нибудь, попробуйте что-нибудь вроде этого:

 @transaction.commit_on_success
 def my_get_or_create(...):
     try:
         obj = MyObj.objects.create(...)
     except IntegrityError:
         transaction.commit()
         obj = MyObj.objects.get(...)
     return obj

Отредактировано 27.05.2010

Есть и второе решение проблемы - использование уровня изоляции READ COMMITED вместо REPEATABLE READ. Но он менее протестирован (по крайней мере, в MySQL), поэтому с ним может быть больше ошибок / проблем - но, по крайней мере, он позволяет привязать представления к транзакциям, не выполняя фиксацию в середине.

Отредактировано 22.01.2012

Вот несколько хороших сообщений в блогах (не моих) о MySQL и Django, связанных с этим вопросом:

http://www.no-ack.org/2010/07/mysql-transactions-and-django.html

http://www.no-ack.org/2011/05/broken-transaction-management-in-mysql.html

person Tomasz Zieliński    schedule 10.02.2010
comment
Вы абсолютно правы. Фиксация транзакции решила проблему. Спасибо :-) - person Hobhouse; 11.02.2010
comment
Есть ли патч обратно к get_or_create в django, ожидающий своего появления? - person StevenC; 21.05.2013
comment
Существуют такие билеты, как code.djangoproject.com/ticket/13906, но проблема нетривиальная. - person Tomasz Zieliński; 22.05.2013
comment
Похоже ссылки сейчас битые :( - person ionelmc; 25.12.2014
comment
Это состояние гонки специфично для mysql? Пострадал бы postgres от той же проблемы? - person Aaron; 29.03.2016

Ваша обработка исключений маскирует ошибку. Вы должны передать значение state в get_or_create() или установить значение по умолчанию в модели и базе данных.

person Ignacio Vazquez-Abrams    schedule 10.02.2010
comment
В то время, когда я запускаю create_or_update_myobj, «владелец» может уже иметь «вещь» в другом «состоянии». В этом случае мне нужно получить существующую «вещь» и изменить «состояние». - person Hobhouse; 10.02.2010
comment
Или у него может не быть состояния any, потому что такой записи нет, и в этот момент он пытается создать новую запись, после чего она сразу же взрывается. - person Ignacio Vazquez-Abrams; 10.02.2010
comment
Интересно, хотя ваш блог является частным, поэтому он не может читать сообщения. - person Stuart Axon; 09.10.2013
comment
@Hobhouse @ Игнасио Васкес-Абрамс. Вы оба наполовину правы. Вам необходимо передать state со значениями по умолчанию kwarg docs .djangoproject.com / en / dev / ref / models / querysets / - person CrazyCasta; 13.06.2016

Один (тупой) способ может заключаться в том, чтобы поймать ошибку и просто повторить попытку один или два раза после небольшого ожидания. Я не эксперт по БД, поэтому может быть сигнальное решение.

person SapphireSun    schedule 10.02.2010

С 2012 года в Django есть select_for_update, который блокирует строки до конца транзакции.

Чтобы избежать состояния гонки в Django + MySQL при обстоятельствах по умолчанию:

  • REPEATABLE_READ в MySQL
  • READ_COMMITTED в Django

вы можете использовать это:

with transaction.atomic():
   instance = YourModel.objects.select_for_update().get(id=42)
   instance.evolve()
   instance.save()

Второй поток будет ждать первого потока (блокировки), и только если первый будет выполнен, второй будет читать данные, сохраненные первым, поэтому он будет работать с обновленными данными.

Затем вместе с get_or_create:

def select_for_update_or_create(...):
    instance = YourModel.objects.filter(
        ...
    ).select_for_update().first()

    if order is None:
        instnace = YouModel.objects.create(...)

    return instance

Функция должна находиться внутри блока транзакции, иначе вы получите от Django: TransactionManagementError: select_for_update нельзя использовать вне транзакции.


Также иногда полезно использовать refresh_from_db() В таких случаях, как:

instance = YourModel.objects.create(**kwargs)
response = do_request_which_lasts_few_seconds(instance)
instance.attr = response.something

вы бы хотели увидеть:

instance = MyModel.objects.create(**kwargs)
response = do_request_which_lasts_few_seconds(instance)
instance.refresh_from_db()  # 3
instance.attr = response.something

и что №3 значительно сократит временное окно возможных состояний гонки, а значит, и шанс на это.

person Sławomir Lenart    schedule 08.04.2020