Django: Як я можу захистити від одночасної модифікації записів бази даних


81

Якщо існує спосіб захисту від одночасних модифікацій однієї і тієї ж бази даних двома або більше користувачами?

Було б прийнятно показувати повідомлення про помилку користувачеві, який виконує другу операцію фіксації / збереження, але дані не повинні перезаписуватися мовчки.

Я думаю, що блокування входу не є можливим, оскільки користувач може скористатися кнопкою "Назад" або просто закрити браузер, залишивши блокування назавжди.


4
Якщо один об’єкт може бути оновлений кількома одночасними користувачами, у вас може виникнути більша проблема з дизайном. Можливо, варто подумати про ресурси, призначені для користувача, або розділити етапи обробки на окремі таблиці, щоб запобігти цьому.
S.Lott

Відповіді:


48

Ось як я роблю оптимістичне блокування в 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


12
Приємно! Однак це не повинно бути "&", а не "&&"?
Giles Thomas,

1
Чи не могли б ви обійти проблему оновлення, що не запускає методи save (), помістивши виклик на "update" у своєму власному перевизначеному методі save ()?
Джонатан Хартлі,

1
Що трапляється, коли два потоки одночасно телефонують filter, обидва отримують однаковий список із немодифікованим e, а потім обидва одночасно телефонують update? Я не бачу семафору, який одночасно блокує фільтрування та оновлення. EDIT: о, я розумію лінивий фільтр. Але яка обґрунтованість припущення, що update () є атомним? безумовно, БД обробляє одночасний доступ
towtwo

1
@totowtwo Я в ACID гарантує замовлення ( en.wikipedia.org/wiki/ACID ). Якщо ОНОВЛЕННЯ виконується над даними, що відносяться до одночасного (але пізніше запущеного) ВИБІР, він буде блокувати, поки ОНОВЛЕННЯ не буде зроблено. Однак кілька SELECT можна виконати одночасно.
Kit Sunde

1
Схоже, це буде працювати належним чином лише в режимі автокомісії (який є типовим). В іншому випадку остаточний COMMIT буде відокремлений від цього оновлення оператора SQL, тому між ними може запускатися паралельний код. І у нас є рівень ізоляції ReadCommited у Django, тому він буде читати стару версію. (Чому я хочу тут здійснити транзакцію вручну - адже я хочу створити рядок в іншій таблиці разом із цим оновленням.) Хороша ідея, однак.
Alex Lokk

39

Це питання трохи застаріле, і моя відповідь трохи пізня, але після того, що я розумію, це було виправлено в Django 1.4 за допомогою:

select_for_update(nowait=True)

див. документи

Повертає набір запитів, який заблокує рядки до кінця транзакції, генеруючи оператор SELECT ... FOR UPDATE SQL у підтримуваних базах даних.

Зазвичай, якщо інша транзакція вже отримала блокування в одному з вибраних рядків, запит буде блокуватися, доки блокування не буде звільнено. Якщо це не та поведінка, яку ви хочете, зателефонуйте select_for_update (nowait = True). Це зробить дзвінок неблокуючим. Якщо конфліктна блокування вже отримана іншою транзакцією, під час оцінки набору запитів буде викликано помилку DatabaseError.

Звичайно, це буде працювати лише в тому випадку, якщо внутрішній сервер підтримує функцію "вибрати для оновлення", яка, наприклад, sqlite не підтримує. На жаль: nowait=TrueMySql не підтримує, тут вам потрібно використовувати:, nowait=Falseякий буде блокувати лише доти, доки блокування не буде звільнено.


2
Це не найкраща відповідь - на запитання явно не хотілося (песимістичного) блокування, і дві відповіді з більшим голосом в даний час зосереджені на оптимістичному контролі паралельності ("оптимістичному блокуванні") з цієї причини. Вибір для оновлення добре в інших ситуаціях.
RichVel

@ giZm0 Це все ще робить його песимістичним блокуванням. Перша нитка, яка отримує замок, може тримати його нескінченно довго.
knaperek

6
Мені подобається така відповідь, оскільки вона стосується документації Django, а не прекрасного винаходу третьої сторони.
anizzomc

29

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

У цих випадках ми зазвичай використовуємо "Оптимістичне блокування". Наскільки мені відомо, Django ORM цього не підтримує. Але були деякі обговорення щодо додавання цієї функції.

Отже, ви самі по собі. В основному, що ви повинні зробити, це додати поле "версія" до вашої моделі та передати його користувачеві як приховане поле. Звичайним циклом оновлення є:

  1. прочитати дані та показати їх користувачеві
  2. користувач змінює дані
  3. користувач розміщує дані
  4. додаток зберігає його назад у базі даних.

Для реалізації оптимістичного блокування, зберігаючи дані, ви перевіряєте, чи версія, яку ви отримали від користувача, така сама, як і версія бази даних, а потім оновлюєте базу даних та збільшуєте версію. Якщо вони не є, це означає, що відбулися зміни після завантаження даних.

Ви можете зробити це за допомогою одного виклику SQL, наприклад:

UPDATE ... WHERE version = 'version_from_user';

Цей дзвінок оновить базу даних лише у тому випадку, якщо версія буде незмінною.


1
Це саме питання також з'явилося на Slashdot. Запропонований вами Оптимістичний блокування також пропонувався там, але трохи краще пояснив imho: hardware.slashdot.org/comments.pl?sid=1381511&cid=29536367
hopla

5
Також зауважте, що ви все-таки хочете використовувати транзакції, щоб уникнути такої ситуації: hardware.slashdot.org/comments.pl?sid=1381511&cid=29536613 Django пропонує проміжне програмне забезпечення для автоматичного обертання кожної дії з базою даних у транзакції, починаючи з з первинного запиту та лише після успішної відповіді: docs.djangoproject.com/en/dev/topics/db/transactions (пам’ятайте: проміжне програмне забезпечення транзакцій лише допомагає уникнути вищезазначеної проблеми з оптимістичним блокуванням, воно не забезпечує блокування сам по собі)
hopla

Я також шукаю деталі, як це зробити. Поки що не везе.
seanyboy

1
Ви можете зробити це за допомогою масових оновлень django. перевір мою відповідь.
Андрій Саву

14

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дозволяє мені мовчки пропускати дубльовані тригери.


1
Чи потрібно обертати вибране для оновлення транзакцією .atomic ()? Якщо я насправді використовую результати для оновлення? Не блокує всю таблицю, роблячи select_for_update noop?
Paul Kenjora

3

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


3
Я, це, це дійсно дивна ідея.
julx

1

Ймовірно, вам слід скористатися проміжним програмним забезпеченням для транзакцій django, навіть незалежно від цієї проблеми.

Що стосується вашої фактичної проблеми, коли кілька користувачів редагують однакові дані ... так, використовуйте блокування. АБО:

Перевірте, до якої версії користувач оновлює (зробіть це надійно, щоб користувачі не могли просто зламати систему, сказавши, що оновлюють останню копію!), І оновлювати лише, якщо ця версія актуальна. В іншому випадку поверніть користувачеві нову сторінку з оригінальною версією, яку вони редагували, надісланою версією та новою версією, написаною іншими. Попросіть їх об’єднати зміни в одну, повністю сучасну версію. Ви можете спробувати автоматично об’єднати їх, використовуючи набір інструментів, як diff + patch, але вам все одно потрібно мати метод ручного злиття, який все одно працює для випадків відмов, тому починайте з цього. Крім того, вам потрібно буде зберегти історію версій і дозволити адміністраторам скасувати зміни, якщо хтось ненавмисно або навмисно переплутає об’єднання. Але у вас, мабуть, це все одно має бути.

Існує велика ймовірність програми / бібліотеки django, яка робить більшу частину цього за вас.


Це також оптимістичне блокування, як запропонував Гійом. Але він, здавалося, отримав усі бали :)
hopla

0

Інше, на що слід звернути увагу, це слово «атомний». Атомна операція означає, що зміна бази даних відбудеться або успішно, або очевидно не вдасться. Швидкий пошук показує це запитання про атомні операції в Django.


Я не хочу виконувати транзакцію або блокування для декількох запитів, оскільки це може зайняти будь-який час (і може взагалі ніколи не закінчитися)
Бер

Якщо транзакція починається, вона повинна закінчитися. Ви повинні заблокувати запис (або розпочати транзакцію, або що б ви не вирішили зробити) лише після того, як користувач натисне кнопку "надіслати", а не тоді, коли він відкриє запис для перегляду.
Харлі Холкомб

Так, але моя проблема в іншому, оскільки два користувачі відкривають одну і ту ж форму, а потім обидва роблять зміни. Я не думаю, що блокування є рішенням цього.
Бер

Ви маєте рацію, але проблема є НЕ існує рішення для цього. Один користувач отримує перемогу, інший - повідомлення про помилку. Чим пізніше ви заблокуєте запис, тим менше проблем у вас виникне.
Harley Holcombe

Я згоден. Я повністю приймаю повідомлення про помилку для іншого користувача. Я шукаю хороший спосіб виявити цей випадок (який, як я очікую, буде дуже рідкісним).
Бер

0

Ідея вище

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.

Мій план полягає в тому, щоб замінити метод _update Manager, який викликається Model.save_base () для виконання оновлення.

Це поточний код у Django 1.3

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

Що потрібно зробити IMHO, це щось на зразок:

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.

Дивно, що модрен-інструменту, як Django, не вистачає вказівок щодо оптимічного контролю надійності.

Я оновлю цю публікацію, коли розгадаю загадку. Будемо сподіватися, що рішення буде мати приємний пітонічний спосіб, який не передбачає тонн кодування, дивних поглядів, пропуску важливих частин Django тощо.


-2

Для безпеки база даних повинна підтримувати транзакції .

Якщо поля "вільної форми", наприклад, текст тощо, і вам потрібно дозволити декільком користувачам мати можливість редагувати однакові поля (ви не можете мати права власності на дані одного користувача), ви можете зберігати вихідні дані в змінна. Коли користувач фіксує, перевірте, чи змінилися вхідні дані від вихідних даних (якщо ні, то не потрібно турбувати БД переписуванням старих даних), якщо вихідні дані порівняно з поточними даними в базі однакові Ви можете зберегти, якщо воно змінилося, Ви можете показати користувачеві різницю та запитати у користувача, що робити.

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

Я не знаю django, тому я не можу дати вам cod3s ..;)


-6

Звідси:
Як запобігти перезапису об’єкта, який хтось інший змінив

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

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()

1
код порушений. умова перегони все ще може виникнути між запитом if check та save. вам потрібно використовувати objects.filter (id = .. & checktamp check) .update (...) та викликати виняток, якщо жоден рядок не оновлено.
Андрій Саву
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.