Подтвердить что ты не робот

Стратегии для решения проблем concurrency, вызванных устаревшими объектами домена (Grails/GORM/Hibernate)

Мне нравится ссылаться на эту проблему как на проблему "повторяемого искателя", потому что она в некотором смысле противоположна "не повторяемому чтению". Поскольку hibernate повторно использует объекты, прикрепленные к его сеансу, результаты поиска могут включать некоторые старые версии объектов, которые теперь устарели.

Проблема заключается в технически разработке Hibernate, но поскольку сеанс Hibernate неявна в доменах Grails и Grails, долговечны (HTTP-запрос длинный для меня). Я решил задать этот вопрос в контексте Grails/GORM.

Я хотел бы спросить экспертов здесь, если есть общепринятые стратегии для решения этой проблемы.

Рассмотрим это:

    class BankAccount {
      String name
      Float amount

      static constraints = {
        name unique: true
      }
    }

и 'componentA':

    BankAccount.findByName('a1')

'код компонента:

    def result = BankAccount.findAll()

Предположим, что компонент A выполняется первым, за которым следует другая логика, за которой следует компонентB, результат из компонента B визуализируется представлением. Компоненты A и B не хотят много знать друг о друге.

Таким образом, компонент resultB содержит старую версию BankAccount 'a1'.

Многие очень неловкие вещи могут произойти. Если BankAccounts были одновременно изменены, представленный список может содержать, например, 2 элемента с именем "a1" (уникальность выглядит у пользователя!), Или денежные переводы между счетами могут отображаться как частично примененная транзакция (если деньги были переведены с a2 на a1, то он будет вычитаться из a2, но еще нет для a1). Эти проблемы смущают и могут снизить доверие пользователей к приложению.

( ADDED 9/24/2014: Вот пример открытия глаз, это утверждение может завершиться неудачно:

  BankAccount.findAllByName('a1').every{ it.name == 'a1' }

Примеры того, как это происходит, можно найти в любом из связанных билетов JIRA или в моем блоге. )

( ДОБАВЛЕНО 9/24/2014: ПРИМЕЧАНИЕ. По-видимому, разумный совет по использованию уникальных ключей с использованием базы данных при реализации метода equals() не безопасен concurrency. Вы можете получить 2 объекта с помощью то же значение "бизнес-ключа", которое отличается.)

Возможные решения, похоже, добавляют много вызовов discard() или много вызововNewSession() и обрабатывают LazyIntializationExeption и DuplicateKeyException и т.д.
Но если я это сделаю, то почему я использую hibernate/GORM? Вызов обновления для каждого объекта, возвращаемого из каждого запроса, кажется просто смешным.

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

С чем это связано с приложениями Grails? Можете ли вы указать мне на какую-либо документацию/обсуждение этой проблемы?

EDITED 9/24/2014: Релевантный билет Grails JIRA: https://jira.grails.org/browse/GRAILS-11645, Hibernate JIRA: https://hibernate.atlassian.net/browse/HHH-9367 (к сожалению, было отклонено), мой блог содержит более подробные примеры: http://rpeszek.blogspot.com/2014/08/i-dont-like-hibernategrails-part-2.html

ДОБАВЛЕНО 10/17/2014: У меня появилось несколько ответов, в которых говорится, что это любое приложение DB/любая проблема ORM. Это неверно.

Верно, что этой проблемы можно избежать, используя длинные транзакции (длина сеанса Hibernate/длина HTTP-запроса) + установка выше, чем нормальный уровень изоляции БД REPEATABLE READ. Это решение просто неприемлемо (почему у нас есть транснациональные сервисы, если для работы приложения должным образом нужны HTTP-запросы длинных транзакций??)

Приложения БД и другие ОРМ не будут демонстрировать эту проблему. Им не понадобятся длительные транзакции, и проблема предотвращается только с помощью READ COMMITTED.

Сейчас уже 2 месяца, так как я разместил этот вопрос здесь, и он не получил значимого ответа. Это просто потому, что у этой проблемы нет ответа. Это то, что Hibernate может исправить, а не то, что приложение Grails может исправить. ADDED 10/17/2014-END

4b9b3361

Ответ 1

Вот моя собственная попытка ответить на этот вопрос.

( ДОБАВЛЕНО 9/24/2014 Нет ничего хорошего в решении этой проблемы. К сожалению, HHH-9367 JIRA билет был отклонен Hibernate как "не ошибка". Единственное предлагаемое решение в этом билете было использовать обновление (я предполагаю, что потребуется изменить все запросы на что-то похожее:

BankAccount.findAllBy...(...).each{ it.refresh() }

Лично я не согласен с тем, что это значимое решение.)

Как я уже объяснял выше, если запрос Hibernate/GORM возвращает набор DomainObjects, а некоторые из них объекты уже находятся в сеансе спящего режима (заполнены предыдущими запросами), запрос вернет эти старые объекты, и эти объекты не будут автоматически обновляться. Это может вызвать некоторые проблемы с обнаружением проблем concurrency. Я называю это проблемой Repeatable Finder.

Это не имеет никакого отношения к кешу второго уровня. Эта проблема возникает из-за того, что hibernate работает даже без настройки кэша второго уровня. (EDITED 9/24/2014: И это не любая проблема ORM, любая проблема с приложением к БД, проблема связана с использованием Hibernate).

Последствия для вашего приложения:

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

Доменные объекты обычно имеют набор связанных ограничений/логических правил, которые должны содержать обычно несколько записей и выполняются либо приложением, либо самой базой данных. Я буду использовать термин из FP и тестирования и назову эти "свойства".

Свойства примера: В приведенном выше примере BankAccount уникальность имени (введенная в действие БД) является свойством (например, вы можете использовать его при определении метода equals()), если деньги передаются между учетными записями, общая сумма денег на этих счетах должна быть постоянной - это свойство.
Если я изменяю свой класс BankAccount и добавляю к нему ассоциацию "branch":

BankBranch branch

Тогда это тоже свойство:

assert BankAccount.findAllByBranch(b).every{it.branch == b}.

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

Повторяющаяся проблема поиска может привести к тому, что большинство свойств будут разбиты при одновременном использовании (страшные вещи!). Например, я повторяю здесь фрагмент кода, который я написал в соответствующий билет JIRA, связанный в вопросе:

... a1 has branch b1
BankAccount.findByName('a1')

... concurrently a1 is moved to branch b2
//fails because stale a1.branch == b1
assert BankAccount.findAllByBranch(b2).every{it.branch == b2} 

Ваше приложение, вероятно, использует явные и неявные свойства и может иметь логику для их принудительного применения. Например, приложение может полагаться на уникальные имена и исключать или возвращать неверные результаты, если они не уникальны (возможно, имя само по себе используется для определения equals()). Это явное использование. Приложение может предоставлять представления списков, и будет очень сложно, если в списке указано свойство, нарушенное (список для учетных записей в ветке b2 показывает некоторые учетные записи с ветвью b1 - это неявное использование). На любой из таких случаев будет влиять "повторяемый искатель".

Если для принудительного использования свойства используется код Grails (а не ограничение DB), то в дополнение к "повторяемому поисковому устройству" необходимо рассмотреть более очевидные проблемы concurrency. (Я не обсуждаю их здесь.)

Поиск проблем:

(Это относится только к сломанным свойствам. Я не знаю, вызывает ли повторяемый поиск другие проблемы.)

Итак, я думаю, что первым шагом будет определение всех свойств в приложении (EDITED: их будет много, потенциально слишком много для изучения), поэтому, сосредоточив внимание на объектах домена, которые могут измениться одновременно, возможно, ключ здесь.), второй шаг - определить, где и как приложение (неявно или явно) использует эти свойства и как они применяются. Код для каждого из этих требований должен быть проверен, чтобы убедиться, что повторяемый искатель не является проблемой.

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

Это, очевидно, нетривиальный процесс, и здесь нет пуленепробиваемых решений.

Проблемы с фиксацией:

Использование discard() для объектов из предыдущих запросов или выполнение запроса, которое зависит от определенного свойства/свойств приложения в отдельном сеансе спящего режима, должно решить проблему. Использование нового подхода к сеансу должно быть более пуленепробиваемым. Я не рекомендую использовать refresh() здесь. (Примечание. Hibernate не предоставляет публичный API для запроса объектов, прикрепленных к его сеансу.)
Использование нового сеанса откроет приложение для некоторые новые проблемы, такие как LazyInitalizationException или DupicateKeyException. Для сравнения это тривиально.

SIDE ПРИМЕЧАНИЕ: Я лично рассматриваю решение каркаса, которое заставляет код ломаться при добавлении дополнительного запроса: страшный недостаток дизайна.

Интересно сравнить Hibernate с Active Record (о котором я знаю гораздо меньше). Hibernate принял подход пуриста ORM, пытаясь сделать RDBMS в OO, Active Record взял подход "ничего общего", находясь ближе к БД и имея дело БД с более сложными проблемами concurrency.
Конечно, в Active Record node.children.first(). Parent!= Parent, но так ли это плохо?
Я признаю, что не понимаю причин решения спящего режима, чтобы не обновлять объекты в кеше при выполнении нового запроса. Были ли они обеспокоены побочными эффектами? Может ли Hibernate и Grails лоббировать это? Потому что это, кажется, ЛУЧШЕЕ долгосрочное решение. (Отредактировано 9/24/2014: мои попытки разрешить Hibernate проблему не удались).

ДОБАВЛЕНО (2014/08/12): Это также могло бы помочь переосмыслить дизайн вашего приложения Grails и использовать GORM/Hibernate только как очень тонкий слой устойчивости. Проектирование такого слоя с жестким контролем над тем, какие запросы выдаются во время каждого запроса, должно свести к минимуму эту проблему. Это, очевидно, не то, что сторонники Grails, (EDITED 9/24/2014, и это только уменьшит, не устранит проблему.)

После долгих размышлений об этом мне кажется, что это может быть большая логическая дыра в стеке технологий Grails/Hibernate. На самом деле нет хорошего решения, если вы заботитесь о concurrency, вы должны быть обеспокоены.

Ответ 2

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

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

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

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

Это не то, что сломано. В любом случае ограничения базы данных должны всегда включать уникальные бизнес-ключи.

Для операций, связанных с обновлениями учетной записи, вы должны либо использовать транзакцию с одной базой данных, которая обеспечивает безопасность путем блокировки (SELECT FOR UPDATE), либо использует оптимистичную блокировку, поэтому, когда другие обновляют ваши данные, вы получите исключение из строя.

Обновление 09/27/2014

Я мог реплицировать ваш вариант использования. Сущность повторно используется в кеше 1-го уровня. Для SQL-запросов у вас есть свобода загрузки одновременных изменений. Пока вы загружаете объекты для их обновления позже, все должно быть хорошо, потому что оптимистический механизм блокировки не позволит вам сохранять устаревшие данные.

Если вы используете HQL/JPQL только для просмотра, вы можете вместо этого использовать прогнозы.

Ответ 3

Хорошая статья от Марка Палмера об этих проблемах. Мне это показалось очень интересным. В конце статьи он дает "решения", которые могут соответствовать потребностям некоторых из вас.

Ложный оптимизм GORM и Hibernate

Ответ 4

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

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

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

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

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

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

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