Статьи

Не ломайте оптимистическую блокировку

Во время Jazoon 2009 я получил несколько минут частного внимания от Майка Кейта к моей последней статье о моделях доменов . Это небольшое время было достойно всей конференции для меня, так как Майк указал пробелы в моем тексте, а также некоторые ценные советы о том, как лучше переводить доменные модели в аннотации JPA. Из этого короткого разговора в моей памяти осталось специальное предложение: не ломай оптимистическую блокировку . После проверки моего исходного кода я согласился с Майком, что игнорирую оптимистическую блокировку в моем слое обслуживания — распространенная ошибка, замеченная в Интернете, а также в разговоре с другими друзьями. Проблема не нова, и решение не является чем-то новым, но я решил вскоре опубликовать ее в своем личном справочнике и, в конечном итоге, за вашу помощь.

Проблема: сломать оптимистическую блокировку.

При представлении моделей доменов через веб-сервисы вы должны сериализовать свои сущности между клиентом и службой, и каждый раз, когда вы делаете это, у вас есть отдельная сущность JPA. Чтобы сохранить отсоединенные объекты в базе данных, вам необходимо повторно присоединить их к новому контексту персистентности — и именно здесь начинается проблема. Параллельные потоки могут обращаться к одному и тому же методу записи, читая одну и ту же сущность, изменяя ее и затем записывая отсоединенную сущность в базу данных. В моем исходном коде я читал последнюю версию объекта, а затем копировал значения полей из внешнего объекта в последний. Таким образом, я гарантировал нерушимый код написания, но я почувствовал самую основную ошибку JPA: я нарушил согласованность сущностей. Из книги Майка: это просто несчастный случай, ожидающий, Ниже вы найдете пример ловушки из моего исходного кода:

    @Override
    public FpUser update(FpUser entity) throws Exception {
        FpUser attached =
            manager.find(FpUser.class, entity.getId());
        <font color="gray">// Here I am modifying the latest entity and not the detached one.</font> 
        attached.setEmail(entity.getEmail());
        attached.setName(entity.getName());
        return manager.merge(attached);
    }

Из приведенного выше кода мы можем перечислить шаги, необходимые для обхода оптимистической блокировки:

  1. Клиент A читает entity.v1
  2. Клиент B читает entity.v1
  3. Клиент A изменяет entity.version1 и запускает update.transaction # 1
  4. Клиент B изменяет entity.version1 и запускает update.transaction # 2
  5. update.transaction # 1 обновляет поля, полученные от клиента A, объединяет сущность, которая получает версию v2, но приостанавливается до завершения.
  6. update.transaction # 2 обновляет поля, полученные от Клиента B, обновляет версию до v3 и завершает возврат entity.v3 Клиенту B.
  7. update.transaction # 1 завершает возврат entity.v2 клиенту A.

В конце вышеуказанного выполнения у нас есть следующий сценарий:

  • Клиент A имеет экземпляр версии 2 сущности
  • Клиент B имеет экземпляр сущности версии 3 (он фактически переключился с версии 1 на 3, даже не заметив изменений в версии 2)
  • База данных содержит данные из версии 3

Хуже всего побочный эффект этой ловушки состоит в том, что клиент А полагает, что текущие данные, сохраненные в базе данных, являются данными версии 2, но на самом деле это неверно, поскольку версия 3 в настоящее время хранится в базе данных. Несовместимость может быть легко обнаружена оптимистической блокировкой JPA, но, поскольку я читаю последнюю версию при каждой операции обновления, код не будет выдавать правильное исключение, и клиенты станут несовместимыми со стороной сервера.

Решение: будь проще

Механизм по умолчанию, указанный в JPA во избежание несоответствий, представляет собой поле Version, применимое к объектам посредством аннотации @Version . После того, как вы включили поле версии в свои объекты, вы можете просто вызвать операцию слияния, чтобы повторно присоединить отдельные объекты, и контейнер будет обрабатывать для вас управление версиями — просто и легко (и безопасно).

Приведенный выше код может быть переписан в звуковой манере:

    @Override
    public FpUser update(FpUser entity) throws Exception {
        return manager.merge(entity);
    }

И все, меньше строк кода с более надежным и надежным кодом. Я исправлю код в репозитории, так что читатели статьи найдут лучший код в репозитории — и, возможно, сотрудники java.net помогут мне добавить приложение к моей статье, предупреждающее об этом читателей. По крайней мере, мы оба знаем об этом отныне ?

Другие интересные блоги о похожих проблемах:

Прежде чем отвести взгляд от следующего блоггера, позвольте мне задать вам интересный вопрос:

Что если я забочусь только частично о своей блокировке сущностей?

Это тема для другой записи в блоге, но во время моего разговора с Майком он признался, что следующий JPA 2.0 включает в себя эту функцию: возможность частично заблокировать сущность. Таким образом, мне не нужно генерировать исключение в транзакции, которое повлияет на поля с второстепенным приоритетом (идея, лежащая в основе общей ловушки, которую я продемонстрировал выше). Люди, которые внедряют код, чтобы избежать исключений во время обновлений, фактически запрещают клиенту получать исключения, решая проблемы блокировки вручную. Эта уловка самоубийства, кажется, имеет смысл, когда некоторые поля поддерживают перезапись данных — обычно необязательные или данные с очень низким приоритетом.

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


Мои каникулы закончились ? время обновить мою рабочую среду и ничего лучше, чем короткий блог, чтобы подогреть мой мозг к третьему кварталу Java в 2009 году. Следующий шаг: завершение второй части статьи, обзор пробелов и предложение качественный материал о Java EE 5 — последний шаг перед началом полной миграции на Java EE 6.

От http://weblogs.java.net/blog/felipegaucho