Оптимистичная блокировка на стороне сервера в приложении ReSTful: обработка асинхронных запросов от одного и того же клиента

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

Само приложение разработано с учетом принципов ReSTful. Он использует Джерси для предоставления доступа к ресурсам и jQuery для представления этих ресурсов пользователю и обеспечения базовых взаимодействий (создание, обновление, удаление). Довольно просто.

В прошлом я успешно использовал JPA и Hibernate для сохранения состояния на стороне сервера в реляционной базе данных, поэтому в данном случае это казалось естественным выбором. С некоторыми незначительными изменениями в модельных объектах я смог получить базовые операции чтения, создания и удаления, работающие в разумные сроки. Однако операция обновления оказалась более сложной.

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

Моя первая проблема заключалась в том, как обновить управляемый объект из базы данных с помощью неуправляемого объекта, поступающего от клиента. Поскольку в данных, отправляемых клиенту, намеренно не указываются ключи базы данных, это было немного утомительно, поскольку сводилось к явному "объединению" элементов из неуправляемого объекта в объект Hibernate. Это не мой вопрос, но если кто-нибудь знает элегантный способ сделать это, мне было бы очень интересно узнать об этом.

Моя вторая проблема, и цель этого поста, возникает, когда я нажимаю кнопку "Сохранить и закрыть" примерно в то же время, когда происходит операция автосохранения, о которой я упоминал ранее. Чаще всего я получаю исключение для оптимистичной блокировки. Это связано с тем, что вторая операция обновления обрабатывается в отдельном потоке, а первая еще обрабатывается.

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

Самый простой подход, который я могу придумать для решения этой проблемы, состоит в том, чтобы переработать часть клиентского Javascript, чтобы гарантировать, что в любой момент времени от этого клиента будет только одна ожидающая операция "обновления" от этого клиента. (Обратите внимание, что если другой клиент обновляет один и тот же ресурс в одно и то же время, исключение для оптимистической блокировки вполне подойдет.) Однако я обеспокоен тем, что принудительное ограничение этого клиента может отклоняться от духа ReST. Разумно ли ожидать, что данный клиент будет иметь не более одного невыполненного запроса "обновления" (PUT) к конкретному ресурсу в любой момент времени в приложении ReSTful?

Это кажется довольно распространенным сценарием, но я не смог найти однозначного ответа о том, как лучше всего с ним справиться. Другие идеи, которые я рассмотрел и отбросил, включают в себя как-то сериализацию запросов от одного и того же клиента (возможно, на основе сеанса HTTP), чтобы они обрабатывались по порядку, реализацию "очереди" обновлений для рабочего потока JPA/Hibernate и вставку новых ". версии ресурса, отслеживая последнюю версию, а не обновляя какую-либо отдельную запись.

Какие-нибудь мысли? Кажется ли разумным ограничение "одно выдающееся обновление за раз" для клиента, или есть лучший подход?

2 ответа

Решение

Я сталкивался с теми же проблемами, когда клиенты одновременно выполняли операции PUT для одного и того же URI. Это не имеет смысла для клиента, но я не думаю, что вы найдете какую-либо жесткую документацию, в которой говорится, что это запрещено, или какой-либо степени RESTful. Действительно, сервер не может ничего делать, кроме как сериализовать их и жаловаться на оптимистичный параллелизм, когда это происходит. Хорошо ведущий себя клиент должен избегать этого, синхронизируя все операции на каждом ресурсе, даже безопасные операции, такие как GET или HEAD, чтобы вы не загрязняли кэш устаревшими данными.

Моя первая проблема заключалась в том, как обновить управляемый объект из базы данных с помощью неуправляемого объекта, поступающего от клиента. Поскольку в данных, отправляемых клиенту, намеренно не указываются ключи базы данных, это было немного утомительно, поскольку сводилось к явному "объединению" элементов из неуправляемого объекта в объект Hibernate. Это не мой вопрос, но если кто-нибудь знает элегантный способ сделать это, мне было бы очень интересно узнать об этом.

Либо вы должны указать идентификатор ресурса, который отправляет клиент, либо использовать PUT (например, PUT /object/{objectId}). Тогда вам не нужно объединять, а просто "заменить". Я предпочитаю всегда возвращаться и ожидать полных ресурсов. Это позволяет избежать отвратительного слияния.

Моя вторая проблема, и цель этого поста, возникает, когда я нажимаю кнопку "Сохранить и закрыть" примерно в то же время, когда происходит операция автосохранения, о которой я упоминал ранее. Чаще всего я получаю исключение для оптимистичной блокировки. Это связано с тем, что вторая операция обновления обрабатывается в отдельном потоке, а первая еще обрабатывается.

Как вы упомянули, вы можете сделать это на стороне клиента с помощью JavaScript. Вы можете ввести флаг "грязный", и и кнопка автосохранения, и кнопка сохранения-закрытия передают запрос на сервер только тогда, когда установлен флаг "грязный". Флаг "грязный" переключается, когда пользователь что-то изменил. Я бы не позволил серверу сериализовать запросы, это все усложняет.

[...] Однако я обеспокоен тем, что навязывание этого ограничения клиенту может отклоняться от духа ReST. Разумно ли ожидать, что данный клиент будет иметь не более одного невыполненного запроса "обновления" (PUT) к конкретному ресурсу в любой момент времени в приложении ReSTful?

Часть духа REST состоит в том, чтобы отделить вещи, и клиент может решить, когда выполнять операции CRUD.

КСТАТИ: Есть отличный интерфейс java-скрипт, который очень хорошо работает с REST apis: extJS. У нас это хорошо работает для внутренних приложений (я бы не стал делать публичный веб-сайт с extJS из-за внешнего вида в стиле extJS).

Другие вопросы по тегам