Что делать, если не работает оптимистическая блокировка?

11

У меня есть следующий сценарий:

  1. Пользователь делает запрос GET/projects/1 и получает ETag .
  2. Пользователь делает запрос PUT/projects/1 с ETag с шага # 1.
  3. Пользователь делает еще один запрос PUT /projects/1с ETag с шага # 1.

Как правило, второй запрос PUT получит ответ 412, поскольку ETag теперь устарел - первый запрос PUT изменил ресурс, поэтому ETag больше не совпадает.

Но что, если два запроса PUT отправляются одновременно (или ровно один за другим)? Первый запрос PUT не успевает обработать и обновить ресурс до прибытия PUT # 2, что приводит к тому, что PUT # 2 перезаписывает PUT # 1. Весь смысл оптимистической блокировки в том, чтобы этого не произошло ...

maximedupre
источник
3
Атомизируйте свои операции в транзакциях бизнес-уровня, как объясняет Эсбен ниже.
Роберт Харви
Что произойдет, если я распылю свои операции, используя транзакции? PUT # 2 не будет обработан, пока PUT # 1 не будет полностью обработан?
maximedupre
7
Стать пессимистом?
jpmc26
ну это то, что блокировка для.
Толстяк
Правильный, конечно, Put № 2 не должен обрабатываться - они должны быть уникальными.
Толстяк

Ответы:

21

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

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

Ли Райан
источник
В настоящее время сервер реализует механизм обнаружения одновременных обновлений путем сравнения хэшей ресурса. Сервер также использует транзакции для всех операций, но я не получаю никаких блокировок, которые могут быть причиной проблемы. Однако в вашем примере, как может быть ошибка в одном из коммитов, если транзакции используют блокировки? Вторая транзакция должна быть ожидающей при чтении состояния, пока первая транзакция не разрешится.
maximedupre
1
@maximedupre: если вы используете транзакцию, у вас есть какие-то блокировки, хотя это могут быть неявные блокировки (блокировки получаются автоматически, когда вы читаете / обновляете поля, а не явно запрашиваете). Механизм, который я описал выше, может быть реализован с использованием только тех неявных блокировок. Как и другой ваш вопрос, это зависит от базы данных, которую вы используете, но многие современные базы данных используют MVCC (управление несколькими версиями параллелизма), чтобы несколько читателей и писателей могли работать с одними и теми же полями без ненужной блокировки друг друга.
Ли Райан
1
Предупреждение: во многой DBMSes (PostgreSQL, Oracle, SQL Server и т.д.), уровень изоляции транзакции по умолчанию «читать совершенный», где ваш подход не достаточно , чтобы предотвратить состояние гонки OP еще. В таких DMBS вы можете исправить это, включив AND ETag = ...в предложение вашего UPDATEоператора WHERE, а затем проверив счетчик обновленных строк. (Или с помощью более жесткого уровня изоляции транзакций, но я не очень рекомендую это делать.)
ruakh,
1
@ruakh: это зависит от того, как вы пишете свой запрос, да, уровень изоляции по умолчанию не обеспечивает такого поведения автоматически для всех запросов, но часто можно структурировать транзакцию так, чтобы этого было достаточно для реализации оптимистической блокировки. В большинстве случаев, если в приложении важна согласованность транзакций, я бы рекомендовал повторное чтение в качестве уровня изоляции по умолчанию; в базах данных, использующих MVCC, накладные расходы на повторяемое чтение довольно минимальны, и это значительно упрощает приложение.
Ли Райан
1
@ruakh: главный недостаток повторяемого чтения - то, что вы должны быть готовы повторить попытку или потерпеть неудачу в случае одновременной транзакции. Обычно это проблема, но приложения, которые обеспечивают Оптимистическую блокировку как стратегию параллелизма, в любом случае уже потребуют такой обработки, поэтому повторяющиеся ошибки чтения естественно отображаются на ошибки оптимистической блокировки, и это фактически не добавляет новых недостатков.
Ли Райан
13

Вы должны выполнить следующую пару атомарно:

  • проверка тега на достоверность (т.е. актуальность)
  • обновление ресурса (включая обновление его тега)

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

Это по-прежнему считается оптимистической блокировкой, если вы посмотрите на общую картину: сам ресурс не заблокирован начальным чтением (GET) каким-либо пользователем или любым пользователем, который просматривает данные, с намерением обновить или нет.

Некоторое атомарное поведение необходимо, но это происходит в рамках одного запроса (PUT), а не попытки удержать блокировку для нескольких сетевых взаимодействий; это оптимистическая блокировка: объект не блокируется GET, но все же может быть безопасно обновлен PUT.

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

Эрик Эйдт
источник
4
+1 за то, что заметил, что это атомарное значение. В зависимости от обновляемого базового ресурса это может быть выполнено без транзакций или блокировки. Например, атомарное сравнение и замена ресурса в памяти или источник событий для постоянных данных.
Аарон М. Эшбах
@ AaronM.Eshbach, согласился, и спасибо, что вызвал их.
Эрик Эйдт
1

Разработчик приложения должен проверить E-Tag и предоставить эту логику. Это не волшебство, что веб-сервер делает для вас, потому что он знает только, как рассчитать E-Tagзаголовки для статического контента. Итак, давайте возьмем ваш сценарий выше и разберем, как должно происходить взаимодействие.

GET /projects/1

Сервер получает запрос, определяет E-Tag для этой версии записи, возвращая ее с фактическим содержимым.

200 - OK
E-Tag: "412"
Content-Type: application/json
{modified: false}

Поскольку у клиента теперь есть значение E-Tag, оно может включать это в PUTзапрос:

PUT /projects/1
If-Match: "412"
Content-Type: application/json
{modified: true}

На этом этапе ваше приложение должно сделать следующее:

  • Убедитесь, что E-Tag по-прежнему правильный: "412" == "412"?
  • Если это так, сделайте обновление и рассчитайте новый E-Tag

Отправьте ответ об успехе.

204 No Content
E-Tag: "543"

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

  • Убедитесь, что E-Tag по-прежнему правильный: "412"! = "543"

В случае ошибки отправьте ответ об ошибке.

412 Precondition Failed

Это код, который вы действительно должны написать. Фактически, E-Tag может быть любым текстом (в пределах, определенных в спецификации HTTP). Это не должно быть число. Это также может быть хеш-значением.

Берин Лорич
источник
Это не стандартная нотация HTTP, которую вы используете здесь. В стандартном совместимом HTTP вы используете ETag только в заголовке ответа. Вы никогда не отправляете ETag в заголовке запроса, а вместо этого используете ранее полученное значение ETag в заголовке If-Match или If-None-Match в заголовках запроса.
Ли Райан
-2

В качестве дополнения к другим ответам я опубликую одну из лучших цитат в документации ZeroMQ, которая точно описывает основную проблему:

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

Под «идеальными программами MT» я подразумеваю код, который легко написать и понять, который работает с одинаковым подходом к проектированию на любом языке программирования и в любой операционной системе и который масштабируется на любое количество ЦП с нулевым состоянием ожидания и без смысла. убывающей отдачи.

Если вы потратили годы на изучение трюков, чтобы заставить ваш код MT работать вообще, не говоря уже о быстром, с блокировками, семафорами и критическими секциями, вы будете испытывать отвращение, когда поймете, что все это было даром. Если есть один урок, который мы извлекли из более чем 30-летнего параллельного программирования, это: просто не делитесь состоянием. Это как двое пьяниц, пытающихся поделиться пивом. Неважно, если они хорошие друзья. Рано или поздно они собираются вступить в бой. И чем больше пьяниц вы добавляете к столу, тем больше они сражаются друг с другом за пиво. Трагическое большинство приложений MT похоже на драки в пьяном баре.

lurscher
источник