Django: как я могу защитить себя от одновременного изменения записей базы данных

Есть ли способ защитить от одновременного изменения одной и той же записи в базе данных двумя или более пользователями?

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

Я думаю, что блокировка записи - это не вариант, так как пользователь может использовать кнопку «Назад» или просто закрыть свой браузер, навсегда оставив блокировку.


person Ber    schedule 26.11.2008    source источник
comment
Если один объект может обновляться несколькими одновременными пользователями, у вас может быть более серьезная проблема с дизайном. Возможно, стоит подумать о пользовательских ресурсах или разделить шаги обработки в отдельные таблицы, чтобы это не было проблемой.   -  person S.Lott    schedule 26.11.2008


Ответы (10)


Вот как я использую оптимистическую блокировку в Django:

updated = Entry.objects.filter(Q(id=e.id) && Q(version=e.version))\
          .update(updated_field=new_value, version=e.version+1)
if not updated:
    raise ConcurrentModificationException()

Перечисленный выше код может быть реализован как метод в Custom Manager.

Я делаю следующие предположения:

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

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

ПРЕДУПРЕЖДЕНИЕ Django Doc:

Имейте в виду, что метод update () преобразуется непосредственно в оператор SQL. Это массовая операция для прямых обновлений. Он не запускает никаких методов save () на ваших моделях и не испускает сигналы pre_save или post_save.

person Andrei Savu    schedule 30.01.2010
comment
Хороший! Разве это не должно быть «&» вместо «&&»? - person Giles Thomas; 11.10.2010
comment
Не могли бы вы обойти проблему «update» без запуска методов save (), поместив вызов «update» в свой собственный переопределенный метод save ()? - person Jonathan Hartley; 22.06.2011
comment
Что происходит, когда два потока одновременно вызывают filter, оба получают одинаковый список с неизмененным e, а затем оба одновременно вызывают update? Я не вижу семафор, который блокирует фильтрацию и обновление одновременно. РЕДАКТИРОВАТЬ: о, теперь я понимаю ленивый фильтр. Но какова обоснованность предположения, что update () является атомарным? конечно, БД обрабатывает одновременный доступ - person totowtwo; 24.06.2011
comment
@totowtwo I в ACID гарантирует порядок (en.wikipedia.org/wiki/ACID). Если UPDATE выполняется для данных, относящихся к параллельному (но запущенному позже) SELECT, он будет заблокирован до тех пор, пока не будет выполнено UPDATE. Однако одновременно может выполняться несколько SELECT. - person Kit Sunde; 21.04.2013
comment
Похоже, это будет правильно работать только в режиме автоматической фиксации (по умолчанию). В противном случае последний COMMIT будет отделен от этого обновляющего оператора SQL, поэтому между ними может выполняться параллельный код. И у нас есть уровень изоляции ReadCommited в Django, поэтому он будет читать старую версию. (Почему мне нужна ручная транзакция здесь - потому что я хочу создать строку в другой таблице вместе с этим обновлением.) Тем не менее, отличная идея. - person Alex Lokk; 30.07.2013
comment
Также можно использовать гибридный подход, сочетающий оптимистичный и пессимистичный параллелизм, который использует лучшее из обоих миров, выполняя логику на этапе чтения, проверяя неизменность строк базы данных, а затем получая блокировку только для фазы записи. Подробнее здесь: github.com/pirate/django-concurrency-talk - person Nick Sweeting; 09.10.2018

Этот вопрос немного устарел, и мой ответ немного запоздал, но после того, что я понимаю, это было исправлено в Django 1.4 с помощью:

select_for_update(nowait=True)

см. документы

Возвращает набор запросов, который блокирует строки до конца транзакции, генерируя SQL-оператор SELECT ... FOR UPDATE для поддерживаемых баз данных.

Обычно, если другая транзакция уже установила блокировку одной из выбранных строк, запрос будет блокироваться до тех пор, пока блокировка не будет снята. Если это не то поведение, которое вам нужно, вызовите select_for_update (nowait = True). Это сделает звонок неблокирующим. Если конфликтующая блокировка уже получена другой транзакцией, при оценке набора запросов будет вызвана ошибка DatabaseError.

Конечно, это будет работать только в том случае, если серверная часть поддерживает функцию «выбрать для обновления», чего, например, нет в sqlite. К сожалению: nowait=True не поддерживается MySql, вы должны использовать: nowait=False, который будет блокироваться только до тех пор, пока блокировка не будет снята.

person giZm0    schedule 21.06.2012
comment
Это не лучший ответ - вопрос явно не требовал (пессимистической) блокировки, и по этой причине два наиболее популярных ответа в настоящее время сосредоточены на оптимистическом управлении параллелизмом (оптимистическая блокировка). Однако выбор для обновления подходит и в других ситуациях. - person RichVel; 15.03.2014
comment
@ giZm0 Это все еще делает блокировку пессимистичной. Первый поток, получивший блокировку, может удерживать ее бесконечно. - person knaperek; 24.06.2014
comment
Мне нравится этот ответ, потому что он связан с документацией Django, а не красивым изобретением какой-либо третьей стороны. - person anizzomc; 22.05.2015

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

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

Так что вы сами по себе. По сути, вам следует добавить в модель поле «версия» и передать его пользователю как скрытое поле. Обычный цикл обновления:

  1. прочитать данные и показать их пользователю
  2. пользователь изменяет данные
  3. пользователь публикует данные
  4. приложение сохраняет его обратно в базу данных.

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

Вы можете сделать это с помощью одного вызова SQL, например:

UPDATE ... WHERE version = 'version_from_user';

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

person Guillaume    schedule 26.11.2008
comment
Этот же вопрос также появился на Slashdot. Оптимистическая блокировка, которую вы предлагаете, также была предложена там, но объяснена немного лучше imho: hardware .slashdot.org / comments.pl? sid = 1381511 & cid = 29536367. - person hopla; 29.09.2009
comment
Также обратите внимание, что вы хотите использовать транзакции поверх этого, чтобы избежать этой ситуации: hardware.slashdot.org/comments.pl?sid=1381511&cid=29536613 Django предоставляет промежуточное ПО для автоматического включения каждого действия в базе данных в транзакцию, начиная с первоначального запроса и фиксируя только после успешного ответа: docs.djangoproject.com/en/dev/topics/db/transactions (обратите внимание: промежуточное программное обеспечение транзакций помогает избежать вышеуказанной проблемы с оптимистической блокировкой, оно не обеспечивает блокировку само по себе) - person hopla; 29.09.2009
comment
Я также ищу подробности о том, как это сделать. Пока не повезло. - person seanyboy; 09.12.2009
comment
вы можете сделать это с помощью массовых обновлений django. проверьте мой ответ. - person Andrei Savu; 30.01.2010

Django 1.11 имеет три удобных варианта для обработки эта ситуация зависит от требований вашей бизнес-логики:

  • Something.objects.select_for_update() будет блокироваться, пока модель не станет свободной
  • Something.objects.select_for_update(nowait=True) и поймать DatabaseError, если модель в настоящее время заблокирована для обновления
  • Something.objects.select_for_update(skip_locked=True) не вернет объекты, которые в данный момент заблокированы

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

«Ожидание» select_for_update очень удобно в последовательных пакетных процессах - я хочу, чтобы они все выполнялись, но пусть они не торопятся. nowait используется, когда пользователь хочет изменить объект, который в настоящее время заблокирован для обновления - я просто скажу им, что он модифицируется в данный момент.

skip_locked полезен для другого типа обновления, когда пользователи могут запускать повторное сканирование объекта - и мне все равно, кто его запускает, пока он запускается, поэтому skip_locked позволяет мне молча пропускать повторяющиеся триггеры.

person kravietz    schedule 18.05.2017
comment
Мне нужно обернуть выбор для обновления с помощью transaction.atomic ()? Если я действительно использую результаты для обновления? Разве он не заблокирует всю таблицу, сделав select_for_update пустым? - person Paul Kenjora; 16.07.2019

Для дальнейшего использования ознакомьтесь с https://github.com/RobCombs/django-locking. Он выполняет блокировку таким образом, чтобы не оставлять постоянных блокировок, путем сочетания разблокировки javascript, когда пользователь покидает страницу, и тайм-аутов блокировки (например, в случае сбоя браузера пользователя). Документация довольно полная.

person Stijn Debrouwere    schedule 02.06.2010
comment
На мой взгляд, это действительно странная идея. - person julx; 25.03.2011

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

Что касается вашей реальной проблемы, когда несколько пользователей редактируют одни и те же данные ... да, используйте блокировку. ИЛИ:

Проверьте, с какой версией обновляется пользователь (делайте это безопасно, чтобы пользователи не могли просто взломать систему, чтобы сказать, что они обновляют последнюю копию!), И обновляйте только в том случае, если эта версия актуальна. В противном случае отправьте пользователю новую страницу с исходной версией, которую он редактировал, представленной версией и новой версией (-ами), написанной другими. Попросите их объединить изменения в одну, полностью обновленную версию. Вы можете попытаться объединить их автоматически, используя набор инструментов, например diff + patch, но вам все равно понадобится ручной метод объединения, работающий в случаях сбоя, так что начните с этого. Кроме того, вам необходимо сохранить историю версий и разрешить администраторам отменять изменения на случай, если кто-то непреднамеренно или намеренно испортит слияние. Но вы, вероятно, все равно должны это иметь.

Скорее всего, есть приложение / библиотека django, которая делает большую часть этого за вас.

person Lee B    schedule 18.09.2009
comment
Это тоже оптимистическая блокировка, как предложил Гийом. Но вроде бы он получил все баллы :) - person hopla; 29.09.2009

Еще одно слово, на которое стоит обратить внимание, - это слово «атомный». Атомарная операция означает, что изменение вашей базы данных либо произойдет успешно, либо явно не удастся. Быстрый поиск показывает этот вопрос об атомарных операциях в Django.

person Harley Holcombe    schedule 26.11.2008
comment
Я не хочу выполнять транзакцию или блокировку нескольких запросов, так как это может занять любое время (и может никогда не закончиться) - person Ber; 26.11.2008
comment
Если транзакция начинается, она должна завершиться. Вам следует заблокировать запись (или начать транзакцию, или что-то еще, что вы решите сделать) только после того, как пользователь нажмет кнопку «Отправить», а не когда они откроют запись для просмотра. - person Harley Holcombe; 26.11.2008
comment
Да, но моя проблема в другом: два пользователя открывают одну и ту же форму, а затем оба фиксируют свои изменения. Я не думаю, что блокировка - это решение этой проблемы. - person Ber; 26.11.2008
comment
Вы правы, но проблема в том, что нет решения для этого. Один пользователь получает победу, другой получает сообщение об ошибке. Чем позже вы заблокируете запись, тем меньше у вас будет проблем. - person Harley Holcombe; 27.11.2008
comment
Я согласен. Я полностью принимаю сообщение об ошибке для другого пользователя. Я ищу хороший способ обнаружить этот случай (который, как я ожидаю, будет очень редким). - person Ber; 27.11.2008

Идея выше

updated = Entry.objects.filter(Q(id=e.id) && Q(version=e.version))\
      .update(updated_field=new_value, version=e.version+1)
if not updated:
      raise ConcurrentModificationException()

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

Проблема в том, как улучшить поведение глухого .save (), чтобы не приходилось вручную подключаться к методу .update ().

Я посмотрел на идею Custom Manager.

Я планирую переопределить метод Manager _update, который вызывается Model.save_base () для выполнения обновления.

Это текущий код в Django 1.3.

def _update(self, values, **kwargs):
   return self.get_query_set()._update(values, **kwargs)

ИМХО нужно сделать что-то вроде:

def _update(self, values, **kwargs):
   #TODO Get version field value
   v = self.get_version_field_value(values[0])
   return self.get_query_set().filter(Q(version=v))._update(values, **kwargs)

Аналогичная вещь должна произойти при удалении. Однако удаление немного сложнее, поскольку Django внедряет в этой области довольно много вуду через django.db.models.deletion.Collector.

Странно, что у инструмента modren, такого как Django, нет руководства по Optimictic Concurency Control.

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

person Kiril    schedule 31.07.2011

В целях безопасности база данных должна поддерживать транзакции.

Если поля имеют "произвольную форму", например текст и т. д., и вам нужно разрешить нескольким пользователям редактировать одни и те же поля (вы не можете владеть данными одним пользователем), вы можете сохранить исходные данные в переменной. Когда пользователь выполняет фиксацию, проверьте, изменились ли входные данные по сравнению с исходными данными (если нет, вам не нужно беспокоить БД, перезаписывая старые данные), если исходные данные по сравнению с текущими данными в БД совпадают вы можете сохранить, если он изменился, вы можете показать пользователю разницу и спросить пользователя, что делать.

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

Я не знаю django, поэтому не могу дать вам cod3s ..;)

person Stein G. Strindhaug    schedule 26.11.2008

Отсюда:
Как предотвратить перезапись объекта кто-то изменил

Я предполагаю, что метка времени будет храниться как скрытое поле в форме, детали которой вы пытаетесь сохранить.

def save(self):
    if(self.id):
        foo = Foo.objects.get(pk=self.id)
        if(foo.timestamp > self.timestamp):
            raise Exception, "trying to save outdated Foo" 
    super(Foo, self).save()
person seanyboy    schedule 09.12.2009
comment
код не работает. состояние гонки все еще может возникать между проверкой if и запросом сохранения. вам нужно использовать objects.filter (id = .. & timestamp check) .update (...) и вызвать исключение, если ни одна строка не была обновлена. - person Andrei Savu; 30.01.2010