Как предотвратить гонки в веб-приложении?

31

Рассмотрим сайт электронной коммерции, где Алиса и Боб редактируют списки продуктов. Алиса улучшает описания, а Боб обновляет цены. Они начинают редактировать Acme Wonder Widget одновременно. Боб заканчивает первым и сохраняет товар по новой цене. Алисе требуется немного больше времени, чтобы обновить описание, и когда она заканчивает, она сохраняет продукт со своим новым описанием. К сожалению, она также перезаписывает цену старой ценой, которая не была предназначена.

По моему опыту, эти проблемы чрезвычайно распространены в веб-приложениях. Некоторое программное обеспечение (например, программное обеспечение Wiki) действительно имеет защиту от этого - обычно второе сохранение завершается неудачно с «страница была обновлена ​​во время редактирования». Но большинство веб-сайтов не имеют этой защиты.

Стоит отметить, что методы контроллера сами по себе являются потокобезопасными. Обычно они используют транзакции базы данных, что делает их безопасными в том смысле, что если Алиса и Боб попытаются сохранить в один и тот же момент, это не приведет к повреждению. Состояние гонки возникает из-за того, что Алиса или Боб имеют устаревшие данные в своем браузере.

Как мы можем предотвратить такие условия гонки? В частности, я хотел бы знать:

  • Какие методы можно использовать? например, отслеживание времени последнего изменения. Каковы плюсы и минусы каждого.
  • Что такое полезный пользовательский опыт?
  • В какие рамки встроена эта защита?
paj28
источник
Вы уже дали ответ: отслеживая дату изменения объектов и сравнивая ее с возрастом данных, которые другие изменения пытаются обновить. Хотите узнать что-то еще, например, как сделать это эффективно?
Килиан Фот
@KilianFoth - я добавил информацию о том, что мне особенно хотелось бы знать
paj28
1
Ваш вопрос ни в коем случае не является особенным для веб-приложений, у настольных приложений может быть точно такая же проблема. Типичные стратегии решения описаны здесь: stackoverflow.com/questions/129329/…
Док Браун
2
К вашему сведению, форма блокировки, которую вы упоминаете в своем вопросе, известна как « оптимистический контроль параллелизма »
TehShrike
Некоторое обсуждение, связанное с Джанго, здесь
paj28

Ответы:

17

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

Фил
источник
Это звучит как наиболее практичный подход. Знаете ли вы какие-либо рамки, которые реализуют это? Я думаю, что самая большая проблема с этой схемой состоит в том, что простое сообщение «конфликт редактирования» расстроит пользователей, но попытка объединить наборы изменений (вручную или автоматически) затруднена.
paj28
К сожалению, я не знаю никаких фреймворков, которые поддерживают это из коробки. Я не думаю, что сообщение об ошибке редактирования confilict будет восприниматься как разочарование, если оно не часто встречается. В конечном счете, это зависит от пользовательской нагрузки системы, когда вы просто проверяете временную метку или реализуете более сложную функцию слияния.
Фил
Я поддерживал продукт распределенных баз данных на ПК, который использовал детальный подход (по сравнению с его локальной копией базы данных): если один пользователь изменил цену, а другой изменил описание - нет проблем! Как в реальной жизни. Если два пользователя изменили цену - 2-й пользователь получает извинения и пытается их изменить снова. Нет проблем! Это не требует блокировок, кроме как в тот момент, когда данные записываются в базу данных. Неважно, будет ли один пользователь идти на ланч, пока их изменения отображаются на экране, и отправляет их позже. Для удаленных изменений базы данных он был основан на отметках времени записи.
1
В Dataflex была функция reread (), которая делает то, что вы описываете. В более поздних версиях это было безопасно в многопользовательской среде. И действительно, это был единственный способ заставить работать такие чередующиеся обновления.
Можете
10

Я видел 2 основных способа:

  1. Добавьте отметку времени последнего обновления страницы, которую вы редактируете, в скрытом вводе. При фиксации отметка времени сверяется с текущей и, если они не совпадают, обновляется кем-то еще и возвращает ошибку.

    • Pro: несколько пользователей могут редактировать разные части страницы. Страница с ошибкой может привести к странице сравнения, где второй пользователь может объединить свои изменения на новой странице.

    • con: иногда большие усилия теряются при больших одновременных изменениях.

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

    • Pro: редактировать усилия не теряются.

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

чокнутый урод
источник
7

Используйте оптимистичный контроль параллелизма .

Добавьте столбец versionNumber или versionTimestamp к рассматриваемой таблице (целое число самое безопасное).

Пользователь 1 читает запись:

{id:1, status:unimportant, version:5}

Пользователь 2 читает запись:

{id:1, status:unimportant, version:5}

Пользователь 1 сохраняет запись, это увеличивает версию:

save {id:1, status:important, version:5}
new value {id:1, status:important, version:6}

Пользователь 2 пытается сохранить прочитанную запись:

save {id:1, status:unimportant, version:5}
ERROR

Hibernate / JPA может сделать это автоматически с @Versionаннотацией

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

Нил Макгиган
источник
Спасибо ... особенно полезно знать о @Version. Один вопрос: почему безопасно хранить состояние в сеансе? В этом случае я бы беспокоился, что использование кнопки «назад» может привести к путанице.
paj28
Сеанс безопаснее, чем скрытый элемент формы, так как пользователь не сможет изменить значение версии. Если это не проблема, тогда проигнорируйте часть о сессии
Нил Макгиган
Эта техника называется Оптимистичный отсутствует замок и находится в SQLAlchemy , а также
paj28
@ paj28 - эта ссылка SQLAlchemyне указывает на оптимистичные офлайновые блокировки, и я не могу найти ее в документации. У вас была более полезная ссылка или вы просто указали людям на SQLAlchemy в целом?
dwanderson
@dwanderson Я имел в виду счетчик версий этой ссылки.
paj28
1

Некоторые системы объектно-реляционного сопоставления (ORM) будут определять, какие поля объекта изменились с момента загрузки из базы данных, и будут создавать оператор обновления SQL, чтобы устанавливать только эти значения. ActiveRecord для Ruby on Rails является одним из таких ORM.

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

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

Грег Бургхардт
источник
Привет Грег. К сожалению, это не помогает в таких гоночных условиях. Если вы рассмотрите мой оригинальный пример, когда Алиса сохраняет данные, ORM увидит столбец цен как грязный и обновит его, даже если обновление не требуется.
paj28
1
@ paj28 Ключевой момент в ответе Грега - « поля, которые пользователь не изменил ». Алиса не изменила цену, поэтому ORM не будет пытаться сохранить значение «цена» в базе данных.
Росс Паттерсон
@RossPatterson - как ORM узнает разницу между полями, которые пользователь изменил, и устаревшими данными из браузера? По крайней мере, без дополнительного отслеживания. Если вы хотите отредактировать ответ Грега, включив в него такое отслеживание, или отправьте другой ответ, это будет полезно.
paj28
@ paj28 какая-то часть системы должна знать, что сделал пользователь, и хранить только те изменения, которые сделал пользователь. Если пользователь изменил цену, затем изменил ее обратно, а затем отправил, это не должно считаться «чем-то, что пользователь изменил», потому что это не так. Если у вас есть система, которая требует такого уровня управления параллелизмом, вы должны построить ее таким образом. Если нет, то нет.
@nocomprende - Конечно, какая-то часть - но не ORM, как говорится в этом ответе
paj28