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.